XMLのパーサーが文字コードエンコーディグを考慮してくれなくて、Readerを作った件

XMLをパースしようとしたところ、うまくいきませんでした。問題のあったソースコードです。

        CXNode node = DominoXMLParser.parse(file, inputStream, progress -> {
            Log.d(MIXAndCCConstant.TAG, "Progress " + progress);

その先です。

        SAXParserFactory saxParserFactory = SAXParserFactory.newInstance();
        SAXParser saxParser = saxParserFactory.newSAXParser();
        InputSource source = new InputSource();
        source.setByteStream(reader);
        saxParser.parse(source, handler);

ここの、

void setByteStream(InputStream byteStream)

について、XMLの最初のタグの文字エンコード指定を無視してしまうのです。
(なにかまちがえたのかもしれないですが、その前提で解決できたのでこれでよしとします。

エンコーディングを理解できるのが、ISO-8859-1などのみだったようです。

そこで、隣にある、

void setCharacterStream(Reader characterStream)

を使うことします。InputStreamからReaderのインスタンスを生成する。
かつ、最初のタグから、Encoding指定をひっぱりだす。

それだけの対応です。

InputStreamCached.java
package org.star_advance.mixandcc.common;

import android.util.Log;

import org.star_advance.mixandcc.MIXAndCCConstant;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.HashMap;

public class InputStreamCached extends InputStream {
    //<?xml version="1.0" encoding="Shift_JIS"?>
    static boolean _localDebug = false;
    public static void test() {
        _localDebug = true;
        test("<?xml no=yes> language=japanese");
        test("<?xml no=yes language=japanese>");
        test("<?xml encoding=\"japanese\"?>");
        test(" <?xml 'encoding'='japanese'");
        test("<?xml encoding=\"japanese\" test=2>");
        test(" <?xml 'encoding'='japanese' text=\"test\"");
        test("<?xml first=1st encoding=\"japanese\" test=2?>");
        test(" <?xml second=\"'777\" encoding='japanese' text=\"test\"");
        test("<?xml 'EnCoding'=english test=2> aaaaaaaaaaaaaaaaaaaaaaaaaaaa=bbbbb");
        test("<?xml encoding=japanese test=1");
    }

    public static void test(String text) {
        try {
            InputStreamCached test = new InputStreamCached(new ByteArrayInputStream(text.getBytes()));
            String line = test.getReader().readLine();
            if (_localDebug)
                Log.e(MIXAndCCConstant.TAG, "read line = " + line);
        }catch(Throwable ex) {
            Log.e(MIXAndCCConstant.TAG, ex.getMessage(), ex);
        }
    }
    InputStream _orginalIn;
    byte[] _buffer;
    int _pos;
    int _length;
    BufferedReader _reader;
    String _encoding = "Shift_JIS";
    public InputStreamCached(InputStream originalIn){
        _orginalIn = originalIn;
        _pos = 0;
        _length = 0;
        _buffer = null;
    }

    public void refill() throws IOException {
        if (_buffer == null) {
            _buffer = new byte[4096];
            _length = _orginalIn.read(_buffer, 0, 4096);
            _pos = 0;
            analyzeEncoding();
        }
        else if (_pos >= _buffer.length) {
            _length = _orginalIn.read(_buffer, 0, 4096);
            _pos = 0;
        }
    }

    public BufferedReader getReader() throws IOException {
        if (_reader != null) {
            return _reader;
        }
        _reader = new BufferedReader(new InputStreamReader(this, _encoding));
        return _reader;
    }

    @Override
    public int read() throws IOException {
        refill();
        if (_pos < _length) {
            return _buffer[_pos++] & 0xff;
        }
        return -1;
    }

    int findString(byte[] buf, int pos, int length, String keyword) {
        int match = 0;
        for (int x = pos; x < length + pos; ++ x) {
            int c1 = (buf[x] & 0x7f);
            int c2=  keyword.charAt(match);
            if (_localDebug)
                Log.e(MIXAndCCConstant.TAG, "match = " + match +" c1 = " + Character.toString(c1) + " c2 = " + Character.toString(c2));
            if (c1 == c2) {
                match ++;
                if (match >= keyword.length()) {
                    return x - pos - match + 1;
                }
            }
            else {
                match = 0;
            }
        }
        return -1;
    }

    static class Decoder {
        Decoder() {

        }
        int inQuote = 0;
        int inEscpae = 0;
        StringBuilder builder = new StringBuilder();

        public boolean isSafeForJudge() {
            if (inQuote == 0 & inEscpae == 0) {
                return true;
            }
            return false;
        }

        public void append(int ch) {
            if (inEscpae == '\\') {
                builder.append((char)ch);
                inEscpae = 0;
                return;
            }
            if (inQuote == '\'') {
                if (ch == '\'') {
                    inQuote = 0;
                    return;
                }
                builder.append((char)ch);
                return;
            }
            else if (inQuote == '\"') {
                if (ch == '\"') {
                    inQuote = 0;
                    return;
                }
                if (ch == '\\') {
                    inEscpae = '\\';
                    return;
                }
                builder.append((char)ch);
                return;
            }
            else if (ch == '\'' || ch == '\"') {
                inQuote = ch;
                return;
            }
            else if (ch == '?') {
                return;
            }
            builder.append((char)ch);
        }
    }

    protected boolean analyzeEncoding() {
        int start = findString(_buffer, 0, _length, "<?xml");
        if (_localDebug)
            Log.e(MIXAndCCConstant.TAG, "findString " + start);
        if (start < 0) {
            return false;
        }
        start += 5;
        if (_localDebug)
            Log.e(MIXAndCCConstant.TAG, "Start Parsing " + start);
        Decoder d = new Decoder();
        boolean readingName = true;
        String name = null;
        HashMap<String, String> attr = new HashMap<>();

        for (int x = start; x < _length; ++ x) {
            int ch = _buffer[x] & 0xff;
            if (d.isSafeForJudge()) {
                if (ch == ' ' || ch == '\t') {
                    if (d.builder.length() == 0) {
                        continue;
                    }
                }
                if (readingName && ch == '='){
                    name = d.builder.toString();
                    d = new Decoder();
                    readingName = false;
                    continue;
                }
                if (!readingName && (ch == ' ' || ch == '\t' || ch == '>')) {
                    String value = d.builder.toString();
                    attr.put(name.toLowerCase(), value);
                    d = new Decoder();
                    readingName = true;
                    if (ch == '>') {
                        break;
                    }
                    continue;
                }
                d.append(ch);
            }
            else {
                d.append(ch);
            }
        }
        if (d.builder.length() > 0) {
            String value = d.builder.toString();
            attr.put(name.toLowerCase(), value);
        }
        if (_localDebug)
            Log.e(MIXAndCCConstant.TAG, "attr = " + attr);
        _encoding = attr.get("encoding");
        if (_encoding == null) {
            _encoding = attr.get("language");
        }
        return _encoding != null;
    }
}

これで今回のページの趣旨はおわります。

こちらの「アプリ:MIXandCC」でも使っている軽いテクニックです。

広告を表示しています。

以下蛇足となります。

呼び出しかたはこのようになります。

InputStream in = context.openFileInput(file);
InputStreamCached converter = new InputStreamCached(in);

CXNode node = DominoXMLParser.parse(file, converter.getReader(), progress -> {
Log.d(MIXAndCCConstant.TAG, "Progress " + progress);
});
DominoXMLParser.java
package org.star_advance.mixandcc.usedomino.xml;

import android.util.Log;

import org.star_advance.mixandcc.MIXAndCCConstant;
import org.star_advance.mixandcc.common.OneHelper;
import org.w3c.dom.Node;
import org.w3c.dom.UserDataHandler;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.Locator;
import org.xml.sax.helpers.DefaultHandler;

import java.io.Reader;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.TreeMap;

import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

public class DominoXMLParser extends DefaultHandler {
    String _name;;
    DominoXMLParser(String name) {
        _name = name;
        _document = new CXNode(null, name);
        _cursor = new LinkedList<>();
        _keep = new TreeMap<>();
    }

    public static CXNode parse(String name, Reader reader, CXFileProgress progress) {
        DominoXMLParser handler = new DominoXMLParser(name);
        try {
            progress.progress("1/5 opening file " + name);
            SAXParserFactory saxParserFactory = SAXParserFactory.newInstance();
            SAXParser saxParser = null;
            saxParser = saxParserFactory.newSAXParser();

            progress.progress("2/5 buffering file");
            InputSource source = new InputSource();
            source.setCharacterStream(reader);
            progress.progress("3/5 parsing XML file");
            saxParser.parse(source, handler);

            progress.progress("4/5 making structure");
            return handler._document;
        } catch (Throwable ex) {
            Log.e(MIXAndCCConstant.TAG, ex.getMessage(), ex);
        }
        return null;
    }

    LinkedList<CXNode> _cursor;
    Locator _locator;
    CXNode _document;

    @Override
    public void startDocument() {
    }

    @Override
    public void setDocumentLocator(Locator locator) {
        _locator = locator;
    }

    TreeMap<String, String> _keep;
    UserDataHandler _dataHandler = new UserDataHandler() {
        @Override
        public void handle(short operation, String key, Object data, Node src, Node dst) {
            switch (operation) {
                case NODE_ADOPTED:
                    break;
                case NODE_CLONED:
                    break;
                case NODE_DELETED:
                    break;
                case NODE_IMPORTED:
                    break;
                case NODE_RENAMED:
                    break;
            }
        }
    };

    public void loopShrink(CXNode target) {
        if (target.listChildren() != null) {
            for (CXNode node : target.listChildren()) {
                String text = node.getTextContent();
                if (text != null) {
                    String newtext = OneHelper.shrinkText(text);
                    if (newtext.length() == 0) {
                        node.setTextContent(null);
                    } else {
                        if (newtext.length() != text.length()) {
                            text = newtext;
                            node.setTextContent(text);
                        }
                    }
                }
                loopShrink(node);
            }
        }
    }
    HashMap<String, String> internCache = new HashMap<>();
    long _hit = 0;
    long _cretaed = 0;

    int _counter = 0;

    public String myIntern(String text) {
        String x = internCache.get(text);
        if (x == null) {
            internCache.put(text, text);
            x = text;
            _cretaed ++;
        }
        else {
            _hit ++;
        }
        if (_counter ++ >= 3000) {
            Log.e(MIXAndCCConstant.TAG, "hit " + _hit + " created "+ _cretaed);
            _counter = 0;
        }
        return x;
    }

    @Override
    public void startElement(String uri, String localName, String qName, Attributes attributes) {
        CXNode parent = _cursor.isEmpty() ? _document : _cursor.getLast();
        CXNode child = new CXNode(parent, qName);

        parent._listChildTags.add(child);
        _cursor.add(child);

        if (attributes.getLength() > 0) {
            for (int i = 0; i < attributes.getLength(); ++i) {
                String name = attributes.getQName(i);
                String value = attributes.getValue(i);
                child._listAttributes.setAttribute(myIntern(name), value, _keep);
            }
        }

        child._lineNumber = _locator.getLineNumber();
        child._columnNumber = _locator.getColumnNumber();
    }

    @Override
    public void characters(char[] ch, int offset, int count) {
        String ret = new String(ch, offset, count);

        if (_cursor == null || _cursor.getLast() == null) {
            return;
        }

        if (ret.length() > 0) {
            String prev = _cursor.getLast().getTextContent();
            if (prev != null) {
                _cursor.getLast().setTextContent(prev + ret);
            } else {
                _cursor.getLast().setTextContent(ret);
            }
        }
    }

    @Override
    public void endElement(String uri, String localName, String qName) {
        if (_cursor.isEmpty() == false) {
            CXNode lastLeaf = _cursor.getLast();
            if (lastLeaf._nodeName.equals(qName)) {
                _cursor.removeLast();

                if (_cursor.isEmpty() == false) {
                    lastLeaf = _cursor.getLast();
                }
            }
        }
    }

    @Override
    public void endDocument() {
        loopShrink(_document);
    }
}

私の場合、XMLをJavaオブジェクトに書き換えて、います。Jsonクラスの実装の説明はここでは割愛します。(自作ライブラリですが、多分既存のクラスを使うほうがいいでしょう。)

CX**の、DOMのようなクラスついては割愛しています。メモリの問題が自作する必要があったとかだと思います。たとえば、実践として、XMLから必要な情報だけを抜いて、クラスインスタンスとして、jsonに書き出す実装はこのようになっています。

DominoSoundModule.java
package org.star_advance.mixandcc.soundmodule;

import android.content.Context;
import android.util.Log;

import org.star_advance.mixandcc.MIXAndCCConstant;
import org.star_advance.mixandcc.common.InputStreamCached;
import org.star_advance.mixandcc.ui.userchoice.UserChoiceElement;
import org.star_advance.mixandcc.ui.userchoice.UserFolderElement;
import org.star_advance.mixandcc.usedomino.xml.CXAttributes;
import org.star_advance.mixandcc.usedomino.xml.CXAttributesElement;
import org.star_advance.mixandcc.usedomino.xml.CXNode;
import org.star_advance.mixandcc.usedomino.xml.DominoXMLParser;

import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;

public class DominoSoundModule extends SoundModule implements UserChoiceElement, UserFolderElement {
    public DominoSoundModule(String name) {
        super(name);
    }

    public boolean loadXML(Context context, String file) {
        InputStream in = null;
        try {
            in = context.openFileInput(file);
            InputStreamCached converter = new InputStreamCached(in);

            CXNode node = DominoXMLParser.parse(file, converter.getReader(), progress -> {
                Log.d(MIXAndCCConstant.TAG, "Progress " + progress);
            });
            if (node != null) {
                parseData(node);
                _fileName = file;
                return true;
            }

        }catch (Throwable ex) {
            Log.e(MIXAndCCConstant.TAG, ex.getMessage(), ex);
        }
        return false;
    }

    public void parseData(CXNode document) {
        _instrument._listMap = new ArrayList<>();

        List<CXNode> moduleData = document.listChildren("ModuleData");
        for (CXNode parent : moduleData) {
            List<CXNode> instrumentList = parent.listChildren("InstrumentList");
            List<CXNode> drumsetList = parent.listChildren("DrumSetList");
            List<CXNode> contorlChangeMacroList = parent.listChildren("ControlChangeMacroList");
            List<CXNode> templateList = parent.listChildren("TemplateList");
            List<CXNode> defaultData = parent.listChildren("DefaultData");
            if (instrumentList != null) {
                for (CXNode instrument : instrumentList) {
                    parseInsturmentList(instrument);
                }
            }
            if (drumsetList != null) {
                for (CXNode drumSet : drumsetList) {
                    parseInsturmentList(drumSet);
                }
            }
            if (contorlChangeMacroList != null) {
                for (CXNode contorlChangeMacro : contorlChangeMacroList) {
                    parseControlChangeMacroList(contorlChangeMacro, null);
                }
            }
        }
    }

    public void parseInstrumentTone(CXNode bankNode, SoundModuleInstrument.BankTag bankTag) {
        CXAttributes attr = bankNode._listAttributes;
        CXAttributesElement name = attr.getElement("name");
        CXAttributesElement key = attr.getElement("key");
        if (name == null || key == null) {
            return;
        }
        String name0 = name.getAsText();
        int key0 = key.getAsInt(-1);
        if (name0.isEmpty() || key0 < 0) {
            return;
        }
        SoundModuleInstrument.ToneTag toneTag = bankTag.createTone(key0, name0);
    }
    public void parseInstrumentBank(CXNode bankNode, SoundModuleInstrument.PCTag pcTag) {
        CXAttributes attr = bankNode._listAttributes;
        CXAttributesElement name = attr.getElement("name");
        CXAttributesElement msb = attr.getElement("msb");
        CXAttributesElement lsb = attr.getElement("lsb");
        if (name == null) {
            return;
        }
        String name0 = name.getAsText();
        int msb0 = (msb == null) ? -1 : msb.getAsInt(-1);
        int lsb0 = (lsb == null) ? -1 : lsb.getAsInt(-1);
        if (name0.isEmpty()) {
            return;
        }
        SoundModuleInstrument.BankTag bankTag = pcTag.createBank(msb0, lsb0, name0);

        List<CXNode> children = bankNode.listChildren("tone");
        if (children != null) {
            for (CXNode child : children) {
                parseInstrumentTone(child, bankTag);
            }
        }
    }
    public void parseInstrumentPC(CXNode pcNode, SoundModuleInstrument.MapTag mapTag) {
        CXAttributes attr = pcNode._listAttributes;
        CXAttributesElement name = attr.getElement("name");
        CXAttributesElement pc = attr.getElement("pc");
        if (name == null || pc == null) {
            return;
        }
        String name0 = name.getAsText();
        int pc0 = pc.getAsInt(0) - 1;
        if (name0.isEmpty() || pc0 < 0) {
            return;
        }

        SoundModuleInstrument.PCTag pcTag = mapTag.createPC(pc0, name0);
        List<CXNode> children = pcNode.listChildren("bank");
        if (children != null) {
            for (CXNode child : children) {
                parseInstrumentBank(child, pcTag);
            }
        }
    }

    public void parseInstrumentMap(CXNode mapNode, SoundModuleInstrument instrument) {
        CXAttributes attr = mapNode._listAttributes;
        CXAttributesElement name = attr.getElement("name");
        String name0 = name == null ? null : name.getAsText();
        if (name0 == null || name0.isEmpty()) {
            return;
        }
        List<CXNode> children = mapNode.listChildren("pc");
        if (children == null) {
            return;
        }

        SoundModuleInstrument.MapTag create = instrument.createMap(name0);

        for (CXNode child : children) {
            parseInstrumentPC(child, create);
        }
    }
    public void parseInsturmentList(CXNode instrumentNode) {
        List<CXNode> listMapNode = instrumentNode.listChildren("Map");
        if (listMapNode == null) {
            return;
        }

        for (CXNode mapNode : listMapNode) {
            parseInstrumentMap(mapNode, _instrument);
        }
    }

    public void addCCtoFolder(SoundModuleCC.FolderTag parentTag, CXNode ccmNode) {
        CXAttributes attr = ccmNode._listAttributes;
        if (attr == null) {
            return;
        }
        CXAttributesElement name = attr.getElement("nam");
        CXAttributesElement id = attr.getElement("id");
        CXAttributesElement color = attr.getElement("color");
        CXAttributesElement sync = attr.getElement("sync");
        if (id == null || name == null) {
            return;
        }
        int id0 = id.getAsInt(-1);
        String name0 = name.getAsText();
        if (id0 < 0 || name0 == null) {
            return;
        }
        if (name0.isEmpty()) {
            return;
        }

        SoundModuleCC.CCMTag ccmTag = parentTag.createCC(id0);
        ccmTag._name = name0;

        CXNode gateNode = ccmNode.firstChild("gate");
        if (gateNode != null) {
            CXAttributes attrGate = gateNode._listAttributes;
            CXAttributesElement min = attrGate.getElement("min");
            if (min != null) {
                ccmTag.gateMin = min.getAsInt(0);
            }
            CXAttributesElement max = attrGate.getElement("max");
            if (max != null) {
                ccmTag.gateMax = max.getAsInt(127);
            }
            CXAttributesElement type = attrGate.getElement("type");
            if (type != null) {
            }
            CXAttributesElement tableId = attrGate.getElement("tableId");
            if (tableId != null) {
            }
            CXAttributesElement sendOffset = attrGate.getElement("sendOffset");
            if (sendOffset != null) {
                ccmTag.gateOffset = sendOffset.getAsInt(0);
            }
            List<CXNode> entry = gateNode.listChildren("Entry");
        }
        CXNode valueNode = ccmNode.firstChild("value");
        if (valueNode != null) {
            CXAttributes attrGate = valueNode._listAttributes;
            CXAttributesElement min = attrGate.getElement("min");
            if (min != null) {
                ccmTag.valueMin= min.getAsInt(0);
            }
            CXAttributesElement max = attrGate.getElement("max");
            if (max != null) {
                ccmTag.valueMax = max.getAsInt(127);
            }
            CXAttributesElement type = attrGate.getElement("type");
            if (type != null) {
            }
            CXAttributesElement tableId = attrGate.getElement("tableId");
            if (tableId != null) {
            }
            CXAttributesElement sendOffset = attrGate.getElement("sendOffset");
            if (sendOffset != null) {
                ccmTag.valueOffset = sendOffset.getAsInt(0);
            }
            List<CXNode> entry = gateNode.listChildren("Entry");
        }
        CXNode memoNode = ccmNode.firstChild("memo");
        if (memoNode != null) {
            ccmTag._memo = memoNode.getTextContent();
        }
        CXNode dataNode = ccmNode.firstChild("data");
        if (dataNode != null) {
            ccmTag.data = dataNode.getTextContent();
        }
    }
    public void parseControlChangeMacroList(CXNode parentNode, SoundModuleCC.FolderTag parentTag) {
        List<CXNode> listChildren = parentNode.listChildren();
        SoundModuleCC.FolderTag rootFolderTag = null;
        if (listChildren != null && listChildren.isEmpty() == false) {
            for (CXNode seekNode : listChildren) {
                if (seekNode._nodeName.equalsIgnoreCase("ccm")) {
                    CXAttributes attr = seekNode._listAttributes;
                    if (attr == null) {
                        continue;
                    }
                    CXAttributesElement name = attr.getElement("nam");
                    CXAttributesElement id = attr.getElement("id");
                    if (id == null || name == null) {
                        continue;
                    }
                    int id0 = id.getAsInt(-1);
                    String name0 = name.getAsText();
                    if (id0 < 0 || name0 == null) {
                        continue;
                    }
                    if (name0.isEmpty()) {
                        continue;
                    }
                    if (parentTag == null) {
                        if (rootFolderTag == null) {
                            rootFolderTag = _cc.createFolder("-");
                        }
                        addCCtoFolder(rootFolderTag, seekNode);
                    }
                    else{
                        addCCtoFolder(parentTag, seekNode);
                    }
                }
                else if (seekNode._nodeName.equalsIgnoreCase("folder")){
                    CXAttributes attr = seekNode._listAttributes;
                    if (attr == null) {
                        continue;
                    }
                    CXAttributesElement name = attr.getElement("name");
                    if (name == null) {
                        continue;
                    }
                    String name0 = name.getAsText();
                    if (name0.isEmpty()) {
                        continue;
                    }
                    if (parentTag == null) {
                        SoundModuleCC.FolderTag folderTag = _cc.createFolder(name0);
                        parseControlChangeMacroList(seekNode, folderTag);
                    } else {
                        SoundModuleCC.FolderTag folderTag = parentTag.createFolder(name0);
                        parseControlChangeMacroList(seekNode, folderTag);
                    }
                }
            }
        }
    }
}

共通ライブラリはそのうちまとめて公開してみるのも一興なのですが、
セキュリティ対策など取られていないので、公開しないかもしれません。

SoundModule.java
package org.star_advance.mixandcc.soundmodule;

import android.content.Context;
import android.util.Log;

import org.star_advance.mixandcc.MIXAndCCConstant;
import org.star_advance.mixandcc.json.JsonHelper;
import org.star_advance.mixandcc.json.JsonHelperSupported;
import org.star_advance.mixandcc.json.MXJsonValue;
import org.star_advance.mixandcc.ui.userchoice.UserChoiceElement;
import org.star_advance.mixandcc.ui.userchoice.UserFolderElement;
import org.star_advance.mixandcc.usedomino.xml.CXFileProgress;

import java.io.InputStream;
import java.io.OutputStream;
import java.io.IOException;
import java.util.ArrayList;

public class SoundModule implements UserChoiceElement, CXFileProgress, UserFolderElement, JsonHelperSupported {
    String _fileName;
    SoundModuleInstrument _instrument;
    SoundModuleCC _cc;
    String _status;

    public String toString() {
        return _fileName + "(" + _instrument._listMap.size() +" maps)";
    }

    public SoundModule(String fileName) {
        if (fileName == null) {
            throw new NullPointerException();
        }
        _fileName = fileName;
        _instrument = new SoundModuleInstrument(this);
        _cc = new SoundModuleCC(this);
        _status = null;
    }
    @Override
    public UserFolderElement getDigParent(){
        return null;
    }

    public String getFileName() {
        return _fileName;
    }

    @Override
    public int getNameRes() {
        return 0;
    }

    @Override
    public int getSubLabel() {
        return 0;
    }

    @Override
    public String getNameText() {
        return _fileName;
    }

    @Override
    public String getSubLabelText() {
        return _status;
    }

    @Override
    public void progress(String text) {
        _status = text;
    }
    public SoundModuleInstrument getInstruments() {
        return _instrument;
    }
    public SoundModuleCC getCCM() {
        return _cc;
    }
    @Override
    public ArrayList<UserChoiceElement> digThisFolder() {
        ArrayList<UserChoiceElement> asFolder = new ArrayList<>();
        asFolder.add(_cc);
        asFolder.add(_instrument);
        return asFolder;
    }

    public boolean loadText(Context context, String file) {
        InputStream in = null;
        try {
            in = context.openFileInput(file);
            return JsonHelper.read(in, this);
        }catch (IOException ex) {
            Log.e(MIXAndCCConstant.TAG, ex.getMessage(), ex);
        }finally {
            if (in != null){
                try {
                    in.close();
                }catch (Throwable ex) {

                }
            }
        }
        return false;
    }

    public boolean saveText(Context context, String name) {
        OutputStream out = null;
        try {
            out = context.openFileOutput(name, Context.MODE_PRIVATE);
            return JsonHelper.write(out, this);
        }catch (IOException ex) {
            Log.e(MIXAndCCConstant.TAG, ex.getMessage(), ex);
        }finally {
            if (out != null){
                try {
                    out.close();
                }catch (Throwable ex) {

                }
            }
        }
        return false;
    }

    public boolean containsBank(int msb) {
        return true;
    }

    @Override
    public String getJsonFolder() {
        return "SoundModule";
    }

    static final int JSON_VERSION = 1;

    @Override
    public boolean loadFromJsonStructure(MXJsonValue.HelperForStructure parent) {
        if (parent.getFollowingInt("JsonVersion", 0) != JSON_VERSION) {
            return false;
        }
        _fileName = parent.getFollowingText("fileName");
        if (_fileName == null) {
            return false;
        }
        MXJsonValue.HelperForStructure instrument = parent.getFollowingStructure("instrument");
        _instrument.loadFromJsonStructure(instrument);
        MXJsonValue.HelperForStructure cc = parent.getFollowingStructure("cc");
        _cc.loadFromJsonStructure(cc);
        return true;
    }

    @Override
    public boolean saveToJsonStructure(MXJsonValue.HelperForStructure parent) {
        parent.setFollowingInt("JsonVersion", JSON_VERSION);
        parent.setFollowingText("fileName", _fileName);
        MXJsonValue.HelperForStructure instrument = parent.addFollowingStructure("instrument");
        _instrument.saveToJsonStructure(instrument);
        MXJsonValue.HelperForStructure cc = parent.addFollowingStructure("cc");
        _cc.saveToJsonStructure(cc);
        return true;
    }
}
SoundModuleCC.java
package org.star_advance.mixandcc.soundmodule;

import org.star_advance.mixandcc.common.MXMidiStatic;
import org.star_advance.mixandcc.json.JsonHelperSupported;
import org.star_advance.mixandcc.json.MXJsonValue;
import org.star_advance.mixandcc.midione.domino.MXMessage;
import org.star_advance.mixandcc.midione.domino.MXMessageFactory;
import org.star_advance.mixandcc.midione.domino.MXTemplate;
import org.star_advance.mixandcc.namedvalue.NamedValue;
import org.star_advance.mixandcc.namedvalue.NamedValueList;
import org.star_advance.mixandcc.namedvalue.NamedValueListFactory;
import org.star_advance.mixandcc.ui.userchoice.UserChoiceElement;
import org.star_advance.mixandcc.ui.userchoice.UserFolderElement;

import java.util.ArrayList;

public class SoundModuleCC implements UserChoiceElement, UserFolderElement, JsonHelperSupported {
    public SoundModuleCC(SoundModule parent) {
        _parent = parent;
        _listFolder = new ArrayList<>();
    }
    SoundModule _parent;
    ArrayList<FolderTag> _listFolder;
    public synchronized FolderTag createFolder(String name) {
        FolderTag folder = new FolderTag(this, name);
        _listFolder.add(folder);
        return folder;
    }

    @Override
    public int getNameRes() {
        return 0;
    }

    @Override
    public int getSubLabel() {
        return 0;
    }

    @Override
    public String getNameText() {
        return "Control Change";
    }

    @Override
    public String getSubLabelText() {
        return null;
    }

    @Override
    public ArrayList<UserChoiceElement> digThisFolder() {
        ArrayList<UserChoiceElement> asFolder = new ArrayList<>();
        asFolder.addAll(_listFolder);
        return asFolder;
    }

    @Override
    public String getJsonFolder() {
        return "SoundModuleCC";
    }

    @Override
    public boolean loadFromJsonStructure(MXJsonValue.HelperForStructure parent) {
        _listFolder.clear();
        MXJsonValue.HelperForArray listFolder = parent.getFollowingArray("listFolder");
        for (int i = 0; i < listFolder.count(); ++ i) {
            MXJsonValue.HelperForStructure folderElement = listFolder.getFollowingStructure(i);
            if (createFolder("-").loadFromJsonStructure(folderElement)== false) {
                return false;
            }
        }
        return true;
    }

    @Override
    public boolean saveToJsonStructure(MXJsonValue.HelperForStructure parent) {
        MXJsonValue.HelperForArray listFolder = parent.addFollowingArray("listFolder");
        for (int i = 0; i < _listFolder.size(); ++ i) {
            MXJsonValue.HelperForStructure folderElement = listFolder.addFollowingStructure();
            if (_listFolder.get(i).saveToJsonStructure(folderElement)== false) {
                return false;
            }
        }
        return true;
    }

    public static class FolderTag implements UserChoiceElement, UserFolderElement, JsonHelperSupported {
        UserFolderElement _parent;
        String _name;
        ArrayList<UserChoiceElement> _listChildren;
        FolderTag(UserFolderElement parent, String name) {
            _parent = parent;
            _name = name;
            _listChildren = new ArrayList<>();
        }

        public synchronized CCMTag createCC(int id) {
            CCMTag cc = new CCMTag(this);
            cc._id = id;
            _listChildren.add(cc);
            return cc;
        }

        public synchronized FolderTag createFolder(String name) {
            FolderTag folder = new FolderTag(this, name);
            _listChildren.add(folder);
            return folder;
        }

        @Override
        public int getNameRes() {
            return 0;
        }

        @Override
        public int getSubLabel() {
            return 0;
        }

        @Override
        public String getNameText() {
            return "[" + _name+ "]";
        }

        @Override
        public String getSubLabelText() {
            if (_listChildren.isEmpty()) {
                return "Empty";
            }
            return "Count " + _listChildren.size();
        }

        @Override
        public ArrayList<UserChoiceElement> digThisFolder() {
            ArrayList<UserChoiceElement> asFolder = new ArrayList<>();
            asFolder.addAll(_listChildren);
            return asFolder;
        }
        @Override
        public UserFolderElement getDigParent(){
            return _parent;
        }

        @Override
        public String getJsonFolder() {
            return "FolderTag";
        }

        @Override
        public boolean loadFromJsonStructure(MXJsonValue.HelperForStructure parent) {
            _name = parent.getFollowingText("name");
            _listChildren.clear();
            MXJsonValue.HelperForArray listChildren = parent.getFollowingArray("listChildren");
            for (int i = 0; i < listChildren.count(); ++ i) {
                MXJsonValue.HelperForStructure childrenElement = listChildren.getFollowingStructure(i);
                String type = childrenElement.getFollowingText("type");
                if (type.equalsIgnoreCase("folder")) {
                    if (createFolder("-").loadFromJsonStructure(childrenElement) == false) {
                        return false;
                    }
                }
                else {
                    if (createCC(0).loadFromJsonStructure(childrenElement) == false) {
                        return false;
                    }
                }
            }
            return true;
        }

        @Override
        public boolean saveToJsonStructure(MXJsonValue.HelperForStructure parent) {
            parent.setFollowingText("name", _name);
            MXJsonValue.HelperForArray listChildren = parent.addFollowingArray("listChildren");
            for (int i = 0; i < _listChildren.size(); ++ i) {
                MXJsonValue.HelperForStructure childrenElement = listChildren.addFollowingStructure();
                UserChoiceElement e = _listChildren.get(i);
                if (e instanceof FolderTag) {
                    FolderTag folderTag = (FolderTag) e;
                    childrenElement.setFollowingText("type", "folder");
                    if (folderTag.saveToJsonStructure(childrenElement) == false) {
                        return false;
                    }
                }
                else {
                    CCMTag ccmTag = (CCMTag) e;
                    childrenElement.setFollowingText("type", "ccm");
                    if (ccmTag.saveToJsonStructure(childrenElement) == false) {
                        return false;
                    }
                }
            }
            return true;
        }
    }

    public static class EntryTag implements UserChoiceElement, UserFolderElement {
        EntryTag(UserFolderElement parent, String label, int value) {
            _parent = parent;
            _label = label;
            _value = value;
        }

        UserFolderElement _parent;
        String _label;
        int _value;

        @Override
        public int getNameRes() {
            return 0;
        }

        @Override
        public int getSubLabel() {
            return 0;
        }

        @Override
        public String getNameText() {
            return _label;
        }

        @Override
        public String getSubLabelText() {
            return Integer.toString(_value);
        }

        @Override
        public ArrayList<UserChoiceElement> digThisFolder() {
            return null;
        }

        @Override
        public UserFolderElement getDigParent() {
            return _parent;
        }
    }
    public static class TableTag implements UserChoiceElement, UserFolderElement{
        int _id;
        UserFolderElement _parent;
        ArrayList<EntryTag> _listEntry;
        TableTag(UserFolderElement parent, int id) {
            _id = id;
            _parent = parent;
            _listEntry = new ArrayList<>();
        }

        @Override
        public int getNameRes() {
            return 0;
        }

        @Override
        public int getSubLabel() {
            return 0;
        }

        @Override
        public String getNameText() {
            return "Table " + _id;
        }

        @Override
        public String getSubLabelText() {
            return _listEntry.size() +" Entries";
        }

        @Override
        public ArrayList<UserChoiceElement> digThisFolder() {
            return null;
        }

        @Override
        public UserFolderElement getDigParent() {
            return _parent;
        }
    }
    public static class CCMTag implements UserChoiceElement, UserFolderElement, JsonHelperSupported {
        public int _id;
        public String _name;
        public int _color;
        public String sync;
        public String data;
        public String _memo;

        public int valueMin = 0;
        public int valueMax = 127;
        public int valueOffset = 0;
        public int valueDefault = 127;
        public int gateMin = 0;
        public int gateMax = 127;
        public int gateOffset = 100;
        public int gateDefault = 0;
        UserFolderElement _parent;

        // Value Data Gate Memo
        CCMTag(UserFolderElement parent) {
            _parent = parent;

        }

        public void read(String name, MXMessage message) {
            _name = name;
            data = message.getTemplateAsText();
            valueMin = message.getValue()._min;
            valueMax = message.getValue()._max;
            valueOffset = 0;
            valueDefault = 100;
            gateMin = message.getGate()._min;
            gateMax = message.getGate()._max;
            gateOffset = 0;
            gateDefault = 100;
        }

        @Override
        public int getNameRes() {
            return 0;
        }

        @Override
        public int getSubLabel() {
            return 0;
        }

        @Override
        public String getNameText() {
            return _name;
        }

        @Override
        public String getSubLabelText() { return data; }

        public String toString() {
            return _name;
        }

        @Override
        public ArrayList<UserChoiceElement> digThisFolder() {
            return null;
        }

        @Override
        public UserFolderElement getDigParent() {
            return _parent;
        }

        @Override
        public String getJsonFolder() {
            return "";
        }

        @Override
        public boolean loadFromJsonStructure(MXJsonValue.HelperForStructure parent) {
            _id = parent.getFollowingInt("id");
            _name = parent.getFollowingText("name");
            _color = parent.getFollowingInt("color");
            sync = parent.getFollowingText("sync");
            data = parent.getFollowingText("data");
            _memo = parent.getFollowingText("memo");

            valueMin = parent.getFollowingInt("valueMin");
            valueMax = parent.getFollowingInt("valueMax");
            valueOffset = parent.getFollowingInt("valueOffset");
            valueDefault = parent.getFollowingInt("valueDefault");
            gateMin = parent.getFollowingInt("gateMin");
            gateMax = parent.getFollowingInt("gateMax");
            gateOffset = parent.getFollowingInt("gateOffset");
            gateDefault = parent.getFollowingInt("gateDefault");
            return true;
        }

        @Override
        public boolean saveToJsonStructure(MXJsonValue.HelperForStructure parent) {
            parent.setFollowingInt("id", _id);
            parent.setFollowingText("name", _name);
            parent.setFollowingInt("color", _color);
            parent.setFollowingText("sync", sync);
            parent.setFollowingText("data", data);
            parent.setFollowingText("memo", _memo);

            parent.setFollowingInt("valueMin", valueMin);
            parent.setFollowingInt("valueMax", valueMax);
            parent.setFollowingInt("valueOffset", valueOffset);
            parent.setFollowingInt("valueDefault", valueDefault);
            parent.setFollowingInt("gateMin", gateMin);
            parent.setFollowingInt("gateMax", gateMax);
            parent.setFollowingInt("gateOffset", gateOffset);
            parent.setFollowingInt("gateDefault", gateDefault);
            return true;
        }
    }

    public MXTemplate prepareTemplate(int[] data) {
        int[] copy = new int[data.length];
        for (int i = 0; i < copy.length; ++i) {
            if (data[i] < 0) {
                copy[i] = 0;
            } else {
                copy[i] = data[i];
            }
        }
        if (copy[0] == MXMidiStatic.COMMAND_SYSEX) {
            int[] copy2 = new int[copy.length + 1];
            for (int i = 0; i < copy.length; ++i) {
                copy2[i + 1] = copy[i];
            }
            copy2[0] = MXMidiStatic.COMMAND2_SYSEX;
            copy = copy2;
        }
        return new MXTemplate(copy);
    }

    public void buildBasic() {
        SoundModuleCC.FolderTag folderTagA = createFolder( "System");
        NamedValueList<Integer> listSystem = NamedValueListFactory.listupSystemModeOneShot(false);
        int id = 500;

        for (NamedValue<Integer> seek : listSystem) {
            int command = seek._value;
            String name = seek._name;
            MXMessage message = MXMessageFactory.fromShortMessage(0, command, 0, 0);

            CCMTag ccmTag = folderTagA.createCC(id++);
            ccmTag.read(name, message);
        }

        NamedValueList<MXTemplate> listReset = new NamedValueList<>();
        listReset.addNameAndValue("GM Reset", prepareTemplate(MXMidiStatic.gmResetSystem));
        listReset.addNameAndValue("GS Reset", prepareTemplate(MXMidiStatic.gsReset));
        listReset.addNameAndValue("XG Reset", prepareTemplate(MXMidiStatic.xgReset));
        listReset.addNameAndValue("Master Volume", prepareTemplate(MXMidiStatic.masterVolume));


        for (NamedValue<MXTemplate> seek : listReset) {
            String name = seek._name;
            MXMessage message = MXMessageFactory.fromTemplate(seek._value);

            CCMTag ccmTag = folderTagA.createCC(id++);
            ccmTag.read(name, message);
        }

        NamedValueList<Integer> listCommand = NamedValueListFactory.listupCommand(false);
        SoundModuleCC.FolderTag folderTag1 = createFolder("Channel Message");

        for (NamedValue<Integer> seek : listCommand) {
            int command = seek._value;
            String name = seek._name;
            MXMessage message;
            if (command == MXMidiStatic.COMMAND_CH_PITCHWHEEL) {
                command = MXMidiStatic.COMMAND2_CH_PITCH_MSBLSB;
                message = MXMessageFactory.fromShortMessage(0, command, MXMidiStatic.CCXML_VH, MXMidiStatic.CCXML_VL);
            } else {
                message = MXMessageFactory.fromShortMessage(0, command, 0, 0);
            }

            CCMTag ccmTag = folderTag1.createCC(id++);
            ccmTag.read(name, message);

            if (message.isCommand(MXMidiStatic.COMMAND2_CH_PITCH_MSBLSB)
                    || message.isCommand(MXMidiStatic.COMMAND_CH_PITCHWHEEL)) {
                ccmTag.valueOffset = 8192;
                ccmTag.valueMin -= 8192;
                ccmTag.valueMax -= 8192;
            }
        }

        SoundModuleCC.FolderTag folderTag2 = createFolder("Control Change");

        id = 2000;
        for (int i = 0; i < 128; ++i) {
            String name = MXMidiStatic.nameOfControlChange(i);
            CCMTag cccmTag = folderTag2.createCC(id ++);
            MXMessage message = MXMessageFactory.fromControlChange(0, 0, i, 0);
            cccmTag.read(name, message);
            if (i == MXMidiStatic.DATA1_CC_PANPOT) {
                cccmTag.valueMin = -64;
                cccmTag.valueMax = 63;
                cccmTag.valueOffset = 64;
            }

        }

        /* https://www.dtmstation.com/wp-content/uploads/2022/05/AMS_MIDI_Impre.pdf ? */
        NamedValueList<String> listDataentry = new NamedValueList<>();
        listDataentry.addNameAndValue("Vibrate Rate", "@NRPN 1 1 #VL 0");
        listDataentry.addNameAndValue("Vibrate Depth", "@NRPN 1 9 #VL 0");
        listDataentry.addNameAndValue("Vibrate Delay", "@NRPN 1 10 #VL 0");
        listDataentry.addNameAndValue("Filter Cutoff", "@NRPN 1 32 #VL 0");
        listDataentry.addNameAndValue("Filter Resonance", "@NRPN 1 33 #VL 0");
        listDataentry.addNameAndValue("Attack Time", "@NRPN 1 99 #VL 0");
        listDataentry.addNameAndValue("Decay Time", "@NRPN 1 100 #VL 0");
        listDataentry.addNameAndValue("Release Time", "@NRPN 1 102 #VL 0");

        listDataentry.addNameAndValue("DrumInst. Pitch", "@NRPN 24 #GL #VL 0");
        listDataentry.addNameAndValue("DrumInst. Level", "@NRPN 26 #GL #VL 0");
        listDataentry.addNameAndValue("DrumInst. Panpot", "@NRPN 28 #GL #VL 0");
        listDataentry.addNameAndValue("DrumInst. Reverve", "@NRPN 29 #GL #VL 0");
        listDataentry.addNameAndValue("DrumInst. Chorus", "@NRPN 30 #GL #VL 0");
        listDataentry.addNameAndValue("DrumInst. Delay", "@NRPN 31 #GL #VL 0");

        int offsetComment = listDataentry.getSize();
        listDataentry.addNameAndValue("PitchBend Range", "@RPN 0 0 #VL 0"); /* 0~24 */
        listDataentry.addNameAndValue("Fine Tuning", "@RPN 0 1 #VH #VL"); /* 0000 - 7f7f = -100 ~ 99.99 cent */
        listDataentry.addNameAndValue("Course Tuning", "@RPN 0 2 #VL 0");   /* 40~88 */
        listDataentry.addNameAndValue("Modulation Depth Range", "@RPN 1 5 #VH #VL"); /* 0~4, 0~127 */

        SoundModuleCC.FolderTag folderTag3 = createFolder("Dataentry");

        for (int i = 0; i < listDataentry.size(); ++i) {
            String name = listDataentry.nameOfIndex(i);
            String text = listDataentry.valueOfIndex(i);
            CCMTag ccmTag = folderTag3.createCC(id ++);
            MXMessage message = MXMessageFactory.fromTemplate(text);
            ccmTag.read(name, message);

            switch (i - offsetComment) {
                case 0:
                    ccmTag.valueMin = 0;
                    ccmTag.valueMax = 24;
                    break;
                case 1:
                    //memoTag.setTextContent("-100 ~ 99.99 cent");
                    break;
                case 2:
                    ccmTag.valueMin = 48;
                    ccmTag.valueMax = 88;
                    break;
                case 3:
                    ccmTag.gateMin = 0;
                    ccmTag.gateMax = 4;
                    break;
                default:
                    break;
            }
        }
    }
    @Override
    public UserFolderElement getDigParent(){
        return _parent;
    }
}
SoundModuleInstrument.java
package org.star_advance.mixandcc.soundmodule;

import android.util.Log;

import androidx.annotation.NonNull;

import org.star_advance.mixandcc.MIXAndCCConstant;
import org.star_advance.mixandcc.common.OneHelper;
import org.star_advance.mixandcc.json.JsonHelperSupported;
import org.star_advance.mixandcc.json.MXJsonValue;
import org.star_advance.mixandcc.midione.MidiOne;
import org.star_advance.mixandcc.ui.userchoice.UserChoiceElement;
import org.star_advance.mixandcc.ui.userchoice.UserFolderElement;
import org.star_advance.mixandcc.usedomino.xml.CXAttributes;
import org.star_advance.mixandcc.usedomino.xml.CXAttributesElement;
import org.star_advance.mixandcc.usedomino.xml.CXNode;

import java.util.ArrayList;
import java.util.List;

public class SoundModuleInstrument implements UserChoiceElement, UserFolderElement, JsonHelperSupported {
    public SoundModuleInstrument(SoundModule parent) {
        _parent = parent;
    }
    SoundModule _parent;

    public SoundModule getSoundModule() {
        return _parent;
    }
    public boolean isEmpty() {
        return _listMap.isEmpty();
    }

    @Override
    public UserFolderElement getDigParent(){
        return _parent;
    }
    String moduleName = null;
    String moduleFolder = null;
    String modulePriority = null;
    String moduleFileCreator = null;
    String moduleFileVersion = null;
    String moduleWebSite = null;

    public void importModule(CXNode module) {
        moduleName = null;
        moduleFolder = null;
        modulePriority = null;
        moduleFileCreator = null;
        moduleFileVersion = null;
        moduleWebSite = null;

        CXAttributes attr = module._listAttributes;
        List<CXAttributesElement> attrElement = attr.internalAccess();
        if (attrElement == null) {
            return;
        }
        for (CXAttributesElement seekAttr : attrElement) {
            String lowerName = seekAttr.getName().toLowerCase();
            String text = seekAttr.getAsText();
            if (text == null || text.length() == 0) {
                continue;
            }
            if (lowerName.equals("name")) {
                moduleName = text;
            } else if (lowerName.equals("folder")) {
                moduleFolder = text;
            } else if (lowerName.equals("priority")) {
                modulePriority = text;
            } else if (lowerName.equals("filecreator")) {
                moduleFileCreator = text;
            } else if (lowerName.equals("fileversion")) {
                moduleFileVersion = text;
            } else if (lowerName.equals("website")) {
                moduleWebSite = text;
            }
        }

        List<CXNode> instrumentList = module.deepSeek("InstrumentList");
        if (instrumentList != null) {
            for (CXNode seekInstrument : instrumentList) {
                List<CXNode> map = seekInstrument.deepSeek("Map");
                if (map != null) {
                    for (CXNode seekMap : map) {
                        importMap(seekMap);
                    }
                }
            }
        }
    }

    public void importMap(CXNode map) {
        String mapName = null;

        CXAttributes attr = map._listAttributes;
        List<CXAttributesElement> attrElement = attr.internalAccess();
        if (attrElement == null) {
            return;
        }
        for (CXAttributesElement seekAttr : attrElement) {
            String lowerName = seekAttr.getName().toLowerCase();
            String text = seekAttr.getAsText();
            if (text == null || text.length() == 0) {
                continue;
            }
            if (lowerName.equals("mapname")) {
                mapName = text;
            }
        }
        MapTag targetMap = findMap(mapName);
        if (targetMap == null) {
            targetMap = createMap(mapName);
        }

        List<CXNode> pcList = map.deepSeek("PC");
        if (pcList != null) {
            for (CXNode seekInst : pcList) {
                importPC(targetMap, seekInst);
            }
        }
    }

    public void importPC(MapTag map, CXNode pc) {
        String pcName = null;
        String pcPc = null;
        CXAttributes attr = pc._listAttributes;
        List<CXAttributesElement> attrElement = attr.internalAccess();
        if (attrElement == null) {
            return;
        }

        for (CXAttributesElement seekAttr : attrElement) {
            String lowerName = seekAttr.getName().toLowerCase();
            String text = seekAttr.getAsText();
            if (text == null || text.length() == 0) {
                continue;
            }
            if (lowerName.equals("pc")) {
                pcPc = text;
            } else if (lowerName.equals("name")) {
                pcName = text;
            }
        }
        int numPc = -1;
        try {
            numPc = Integer.parseInt(pcPc);
        } catch (Throwable ex) {

        }
        if (pcPc == null) {
            return;
        }
        if (pcPc.length() == 0) {
            return;
        }
        numPc--;
        if (numPc < -1) {
            numPc = -1;
        }

        PCTag targetPc = map.findPC(numPc);
        if (targetPc == null) {
            targetPc = map.createPC(numPc, pcName);
        } else {
            targetPc._name = pcName;
        }

        List<CXNode> bankList = pc.deepSeek("Bank");
        if (bankList != null) {
            for (CXNode bank : bankList) {
                importBank(targetPc, bank);
            }
        } else {
            BankTag targetBank = targetPc.findBank(-1, -1);
            if (targetBank == null) {
                String bankName = pcName;
                targetPc.createBank(-1, -1, bankName);
            }
        }
    }

    ;

    public void importBank(PCTag pc, CXNode bank) {
        String bankName = null;
        String bankMSB = null;
        String bankLSB = null;

        CXAttributes attr = bank._listAttributes;
        List<CXAttributesElement> attrElement = attr.internalAccess();
        if (attrElement == null) {
            return;
        }

        for (CXAttributesElement seekAttr : attrElement) {
            String lowerName = seekAttr.getName().toLowerCase();
            String text = seekAttr.getAsText();
            if (text == null || text.length() == 0) {
                continue;
            }
            if (lowerName.equals("name")) {
                bankName = text;
            } else if (lowerName.equals("msb")) {
                bankMSB = text;
            } else if (lowerName.equals("lsb")) {
                bankLSB = text;
            }
        }
        int numMSB = -1;
        try {
            numMSB = Integer.parseInt(bankMSB);
        } catch (Throwable ex) {

        }
        int numLSB = -1;
        try {
            numLSB = Integer.parseInt(bankLSB);
        } catch (Throwable ex) {

        }
        if (bankName == null) {
            return;
        }
        if (bankName.isEmpty()) {
            return;
        }

        BankTag targetBank = pc.findBank(numMSB, numLSB);
        if (targetBank == null) {
            targetBank = pc.createBank(numMSB, numLSB, bankName);
        } else {
            targetBank._name = bankName;
        }
        List<CXNode> toneList = bank.deepSeek("Tone");
        if (toneList != null) {
            for (CXNode tone : toneList) {
                importTone(targetBank, tone);
            }
        }
    }

    public void importTone(BankTag bank, CXNode tone) {
        String toneKey = null;
        String toneName = null;

        CXAttributes attr = tone._listAttributes;
        List<CXAttributesElement> attrElement = attr.internalAccess();
        if (attrElement == null) {
            return;
        }
        for (CXAttributesElement seekAttr : attrElement) {
            String lowerName = seekAttr.getName().toLowerCase();
            String text = seekAttr.getAsText();
            if (text == null || text.length() == 0) {
                continue;
            }
            if (lowerName.equals("name")) {
                toneName = text;
            } else if (lowerName.equals("key")) {
                toneKey = text;
            }
        }
        int numKey = -1;
        try {
            numKey = Integer.parseInt(toneKey);
        } catch (Throwable ex) {

        }
        if (numKey < 0) {
            return;
        }

        ToneTag targetTone = bank.findTone(numKey);
        if (targetTone == null) {
            targetTone = bank.createTone(numKey, toneName);
        } else {
            targetTone._name = toneName;
        }
    }


    public ArrayList<MapTag> listMap() {
        return _listMap;
    }
    ArrayList<MapTag> _listMap;

    public synchronized MapTag findMap(String name) {
        if (_listMap == null) {
            return null;
        }
        for (MapTag seekMap : _listMap) {
            if (seekMap._name.equals(name)) {
                return seekMap;
            }
        }
        return null;
    }

    public synchronized MapTag createMap(String name) {
        if (_listMap == null) {
            _listMap = new ArrayList<>();
        }
        if (MidiOne.isDebug) {
            if (findMap(name) != null) {
                throw new IllegalStateException("use findMap before createMap");
            }
        }
        MapTag newMap = new MapTag(this);
        newMap._name = name;
        _listMap.add(newMap);
        return newMap;
    }

    @Override
    public int getNameRes() {
        return 0;
    }

    @Override
    public int getSubLabel() {
        return 0;
    }

    @Override
    public String getNameText() {
        return "Instrument";
    }

    @Override
    public String getSubLabelText() {
        return null;
    }

    @Override
    public ArrayList<UserChoiceElement> digThisFolder() {
        ArrayList<UserChoiceElement> asFolder = new ArrayList<>();
        asFolder.addAll(_listMap);
        return asFolder;
    }

    @Override
    public String getJsonFolder() {
        return "Instrument";
    }

    @Override
    public boolean loadFromJsonStructure(MXJsonValue.HelperForStructure parent) {
        moduleName = parent.getFollowingText("moduleName");
        moduleFolder= parent.getFollowingText("moduleFolder");
        modulePriority= parent.getFollowingText("modulePriority");
        moduleFileCreator= parent.getFollowingText("moduleFileCreator");
        moduleFileVersion= parent.getFollowingText("moduleFileVersion");
        moduleWebSite= parent.getFollowingText("moduleWebSite");
        MXJsonValue.HelperForArray listMap = parent.getFollowingArray("listMap");
        _listMap = new ArrayList<>();
        for (int i= 0; i < listMap.count(); ++ i) {
            MXJsonValue.HelperForStructure mapElement = listMap.getFollowingStructure(i);
            String mapName = mapElement.getFollowingText("name");
            if (createMap(mapName).loadFromJsonStructure(mapElement) == false) {
                return false;
            }
        }
        return true;
    }

    @Override
    public boolean saveToJsonStructure(MXJsonValue.HelperForStructure parent) {
        parent.setFollowingText("moduleName", moduleName);
        parent.setFollowingText("moduleFolder", moduleFolder);
        parent.setFollowingText("modulePriority", modulePriority);
        parent.setFollowingText("moduleFileCreator", moduleFileCreator);
        parent.setFollowingText("moduleFileVersion", moduleFileVersion);
        parent.getFollowingText("moduleWebSite", moduleWebSite);
        MXJsonValue.HelperForArray listMap = parent.addFollowingArray("listMap");
        for (int i= 0; i < _listMap.size(); ++ i) {
            MXJsonValue.HelperForStructure mapElement = listMap.addFollowingStructure();
            if (_listMap.get(i).saveToJsonStructure(mapElement) == false) {
                return false;
            }
        }
        return true;
    }

    public static class MapTag implements UserChoiceElement, UserFolderElement, JsonHelperSupported{
        public MapTag(SoundModuleInstrument parent) {
            _parent = parent;
        }

        public SoundModuleInstrument getParent() {
            return  _parent;
        }
        SoundModuleInstrument _parent;
        public String _name;
        public PCTag[] _listPC = new PCTag[256];
        public int _listPCLength = 0;
        public synchronized PCTag findPC(int pc) {
            if (pc < 0 || pc >= 256) {
                return null;
            }
            return _listPC[pc];
        }
        public synchronized PCTag createPC(int pc, String name) {
            if (_listPC[pc] != null) {
                return _listPC[pc];
            }
            PCTag newPC = new PCTag(this);
            newPC._pc = pc;
            newPC._name = name;
            _listPC[pc] = newPC;
            if (pc >= _listPCLength) {
                _listPCLength = pc + 1;
            }
            return newPC;
        }

        public synchronized int getCount() {
            int count = 0;
            for (int i = 0; i < _listPCLength; ++ i) {
                if (_listPC[i] != null) {
                    count++;
                }
            }
            return count;
        }

        @Override
        public String toString() {
            if (_parent == null) {
                return "-";
            }
            return getNameText() + " (" + getCount() + " PC)";
        }

        @Override
        public int getNameRes() {
            return 0;
        }

        @Override
        public int getSubLabel() {
            return 0;
        }

        @Override
        public String getNameText() {
            return "Map: " + _name;
        }

        @Override
        public String getSubLabelText() {
            if (_listPC == null) {
                return "Empty";
            }

            return "PC count:" + getCount();
        }

        @Override
        public ArrayList<UserChoiceElement> digThisFolder() {
            ArrayList<UserChoiceElement> asFolder = new ArrayList<>();
            for (int i = 0; i < _listPCLength; ++ i) {
                if (_listPC[i] != null) {
                    asFolder.add(_listPC[i]);
                }
            }
            return asFolder;
        }
        @Override
        public UserFolderElement getDigParent(){
            return _parent;
        }

        @Override
        public String getJsonFolder() {
            return "MapTag";
        }

        @Override
        public boolean loadFromJsonStructure(MXJsonValue.HelperForStructure parent) {
            _name = parent.getFollowingText("name");
            MXJsonValue.HelperForArray listPC = parent.getFollowingArray("listPC");
            _listPC = new PCTag[_listPC.length];
            for (int i = 0; i < listPC.count(); ++ i) {
                MXJsonValue.HelperForStructure pcElement = listPC.getFollowingStructure(i);
                int pc = pcElement.getFollowingInt("pc");
                String name = pcElement.getFollowingText("name");
                if (createPC(pc, name).loadFromJsonStructure(pcElement) == false) {
                    return false;
                }
            }
            return true;
        }

        @Override
        public boolean saveToJsonStructure(MXJsonValue.HelperForStructure parent) {
            parent.setFollowingText("name", _name);
            MXJsonValue.HelperForArray listPC = parent.addFollowingArray("listPC");
            for (int i = 0; i < _listPC.length; ++ i) {
                PCTag pcTag = _listPC[i];
                if (pcTag == null) {
                    continue;
                }
                MXJsonValue.HelperForStructure pcElement = listPC.addFollowingStructure();

                if (pcTag.saveToJsonStructure(pcElement) == false) {
                    return false;
                }
            }
            return true;
        }
    }

    public static class PCTag implements UserChoiceElement, UserFolderElement, JsonHelperSupported {
        PCTag(MapTag parent) {
            _parent = parent;
        }

        public MapTag getParent() {
            return _parent;
        }

        MapTag _parent;
        public String _name;
        public int _pc;
        ArrayList<BankTag> _listBank;

        public synchronized BankTag findBank(int msb, int lsb) {
            if (_listBank == null) {
                return null;
            }
            msb &= 0xff;
            lsb &= 0xff;
            for (BankTag seekBank : _listBank) {
                int msb2 = seekBank._msb & 0xff;
                int lsb2 = seekBank._lsb & 0xff;
                if (msb2 == msb && lsb2 == lsb) {
                    return seekBank;
                }
            }
            return null;
        }

        public synchronized BankTag createBank(int msb, int lsb, String name) {
            if (_listBank == null) {
                _listBank = new ArrayList<>();
            }
            BankTag newBank = new BankTag(this);
            newBank._msb = msb;
            newBank._lsb = lsb;
            newBank._name = name;
            _listBank.add(newBank);
            return newBank;
        }

        @Override
        @NonNull
        public String toString() {
            return getNameText() +" ("  + _listBank.size() + " banks)";
        }

        @Override
        public int getNameRes() {
            return 0;
        }

        @Override
        public int getSubLabel() {
            return 0;
        }

        @Override
        public String getNameText() {
            return _pc +":" + _name;
        }

        @Override
        public String getSubLabelText() {
            if (_listBank == null) {
                return "Empty";
            }
            return "Bank count:" +_listBank.size();
        }

        public ArrayList<BankTag> listBank() {
            return _listBank;
        }
        @Override
        public ArrayList<UserChoiceElement> digThisFolder() {
            ArrayList<UserChoiceElement> asFolder = new ArrayList<>();
            asFolder.addAll(_listBank);
            return asFolder;
        }
        @Override
        public UserFolderElement getDigParent(){
            return _parent;
        }

        @Override
        public String getJsonFolder() {
            return "PCTag";
        }

        @Override
        public boolean loadFromJsonStructure(MXJsonValue.HelperForStructure parent) {
            _name = parent.getFollowingText("name");
            _pc = parent.getFollowingInt("pc");
            MXJsonValue.HelperForArray listBank = parent.getFollowingArray("listBank");
            _listBank = null;
            for (int i = 0; i < listBank.count(); ++ i) {
                MXJsonValue.HelperForStructure pcElement = listBank.getFollowingStructure(i);
                if (createBank(0, 0, null).loadFromJsonStructure(pcElement) == false) {
                    return false;
                }
            }
            return true;
        }

        @Override
        public boolean saveToJsonStructure(MXJsonValue.HelperForStructure parent) {
            parent.setFollowingInt("pc", _pc);
            parent.setFollowingText("name", _name);
            MXJsonValue.HelperForArray listBank = parent.addFollowingArray("listBank");
            for (int i = 0; i < _listBank.size(); ++ i) {
                MXJsonValue.HelperForStructure bankElement = listBank.addFollowingStructure();
                BankTag bank = _listBank.get(i);
                if (bank.saveToJsonStructure(bankElement) == false) {
                    return false;
                }
            }
            return true;
        }
    }

    public static BankTag createNeutral(int pc, int msb, int lsb) {
        String name = "PC=" + pc + " Bank=" + OneHelper.toHexString2(msb) + ":" + OneHelper.toHexString2(lsb);
        PCTag pcTag = new PCTag(null);
        pcTag._pc = pc;
        pcTag._name = name;
        BankTag bankTag = new BankTag(pcTag);
        bankTag._msb = msb;
        bankTag._lsb = lsb;
        bankTag._name = name;
        return bankTag;
    }

    public static class BankTag implements UserChoiceElement ,UserFolderElement, JsonHelperSupported {
        BankTag(PCTag parent) {
            _parent = parent;
        }

        public PCTag getParent() {
            return _parent;
        }

        PCTag _parent;

        public String _name;
        public int _msb;
        public int _lsb;
        ArrayList<ToneTag> _listTone;
        public synchronized ToneTag findTone(int key) {
            if (_listTone == null) {
                return null;
            }
            for (ToneTag seekTone : _listTone) {
                if (seekTone._key == key) {
                    return seekTone;
                }
            }
            return null;
        }

        public synchronized ToneTag createTone(int key, String name) {
            if (_listTone == null) {
                _listTone = new ArrayList<>();
            }
            if (MidiOne.isDebug) {
                if (findTone(key) != null) {
                    throw new IllegalStateException("use findEtone before entyrTone");
                }
            }
            ToneTag newTone = new ToneTag(this);
            newTone._key = key;
            newTone._name = name;
            _listTone.add(newTone);
            return newTone;
        }

        @Override
        @NonNull
        public String toString() {
            return getNameText();
        }

        @Override
        public int getNameRes() {
            return 0;
        }

        @Override
        public int getSubLabel() {
            return 0;
        }

        @Override
        public String getNameText() {
            return _name + "[" + OneHelper.toHexString2(_msb & 0x0ff) + ":" + OneHelper.toHexString2( _lsb & 0x0ff)+"]";
        }

        @Override
        public String getSubLabelText() {
            if (_listTone == null) {
                return null;
            }
            return "Tone count:" + _listTone.size();
        }

        public ArrayList<ToneTag> listTone() {
            return _listTone;
        }

        @Override
        public ArrayList<UserChoiceElement> digThisFolder() {
            if (_listTone == null) {
                return null;
            }
            if (_listTone.isEmpty()) {
                return null;
            }
            ArrayList<UserChoiceElement> asFolder = new ArrayList<>();
            asFolder.addAll(_listTone);
            return asFolder;
        }
        @Override
        public UserFolderElement getDigParent(){
            return _parent;
        }

        @Override
        public String getJsonFolder() {
            return "BankTag";
        }

        @Override
        public boolean loadFromJsonStructure(MXJsonValue.HelperForStructure parent) {
            _name = parent.getFollowingText("name");
            _msb  = parent.getFollowingInt("msb");
            _lsb  = parent.getFollowingInt("lsb");
            MXJsonValue.HelperForArray listTone = parent.getFollowingArray("listTone");
            _listTone = null;
            if (listTone != null) {
                for (int i = 0; i < listTone.count(); ++ i) {
                    MXJsonValue.HelperForStructure toneElement = listTone.getFollowingStructure(i);
                    if (createTone(0, null).loadFromJsonStructure(toneElement) == false) {
                        return false;
                    }
                }
            }
            return true;
        }

        @Override
        public boolean saveToJsonStructure(MXJsonValue.HelperForStructure parent) {
            parent.setFollowingText("name", _name);
            parent.setFollowingInt("msb", _msb);
            parent.setFollowingInt("lsb", _lsb);
            if (_listTone != null) {
                MXJsonValue.HelperForArray listTone = parent.addFollowingArray("listTone");
                for (int i = 0; i < _listTone.size(); ++ i) {
                    MXJsonValue.HelperForStructure toneElement = listTone.addFollowingStructure();
                    if (_listTone.get(i).saveToJsonStructure(toneElement) == false) {
                        return false;
                    }
                }
            }
            return true;
        }
    }

    public static class ToneTag implements UserChoiceElement,UserFolderElement,JsonHelperSupported {
        ToneTag(BankTag parent) {
            _parent = parent;
        }

        public BankTag getParent() {
            return _parent;
        }

        BankTag _parent;
        public String _name;
        public int _key;

        @Override
        @NonNull
        public String toString() {
            return getNameText();
        }

        @Override
        public int getNameRes() {
            return 0;
        }

        @Override
        public int getSubLabel() {
            return 0;
        }

        @Override
        @NonNull
        public String getNameText() {
            return _key +":" + _name;
        }

        @Override
        public String getSubLabelText() {
            return null;
        }

        @Override
        public ArrayList<UserChoiceElement> digThisFolder() {
            return null;
        }

        @Override
        public UserFolderElement getDigParent() {
            return _parent;
        }

        @Override
        public String getJsonFolder() {
            return "ToneTag";
        }

        @Override
        public boolean loadFromJsonStructure(MXJsonValue.HelperForStructure parent) {
            _name = parent.getFollowingText("name");
            _key  = parent.getFollowingInt("key");
            return true;
        }

        @Override
        public boolean saveToJsonStructure(MXJsonValue.HelperForStructure parent) {
            parent.setFollowingText("name", _name);
            parent.setFollowingInt("key", _key);
            return true;
        }
    }

    @Override
    @NonNull
    public String toString() {
        return String.valueOf(_listMap);
    }

    public BankTag getFirstEntry(int channel) {
        if (_listMap == null) {
            return null;
        }
        BankTag matchChProg = null;
        BankTag matchProg = null;

        for (MapTag mapTag : _listMap) {
            for (int x =0 ; x < mapTag._listPCLength; ++ x) {
                PCTag pcTag = mapTag._listPC[x];
                if (pcTag == null) {
                    continue;
                }
                for (BankTag bank : pcTag._listBank) {
                    if (matchProg == null) {
                        matchProg = bank;
                    }
                    if (channel == 9) {
                        if (bank._msb >= 120) {
                            if (matchChProg != null) {
                                matchChProg = bank;
                            }
                        }
                    }
                }
            }
        }
        if (matchChProg != null) {
            return matchChProg;
        }
        return matchProg;
    }

    public BankTag findEntry(int channel, int prog, int msb, int lsb) {
        if (_listMap == null) {
            return null;
        }
        BankTag matchChProgBank = null;
        BankTag matchProgBank = null;
        BankTag matchChProg = null;
        BankTag matchProg = null;

        for (MapTag map : _listMap) {
            PCTag pc = map._listPC[prog];
            if (pc == null) {
                continue;
            }
            for (BankTag bank : pc._listBank) {
                if (matchProg == null) {
                    matchProg = bank;
                }
                if (bank._msb == msb && bank._lsb == lsb) {
                    if (matchProgBank != null) {
                        matchProgBank = bank;
                    }
                    if (channel == 9) {
                        if (bank._msb >= 120) {
                            if (matchChProgBank != null) {
                                matchChProgBank = bank;
                            }
                        }
                    }
                } else if (channel == 9) {
                    if (bank._msb >= 120) {
                        if (matchChProg != null) {
                            matchChProg = bank;
                        }
                    }
                }
            }
        }
        if (matchChProgBank != null) {
            return matchChProgBank;
        }
        if (matchChProg != null) {
            return matchChProg;
        }
        if (matchProgBank != null) {
            return matchProgBank;
        }
        return matchProg;
    }

    public ToneTag simpleFindDrum(int key) {
        for (MapTag mapTag : _listMap) {
            for (int x =0 ; x < mapTag._listPCLength; ++ x) {
                PCTag pcTag = mapTag._listPC[x];
                if (pcTag == null) {
                    continue;
                }
                if (pcTag._listBank == null) {
                    continue;
                }
                for (BankTag bankTag : pcTag._listBank) {
                    if (bankTag._listTone == null) {
                        continue;
                    }
                    for (ToneTag toneTag : bankTag._listTone) {
                        if (toneTag._key == key) {
                            return toneTag;
                        }
                    }
                }
            }
        }
        return null;
    }

    public BankTag findEntryCH10(int channel, int seekPC, int bank) {
        if (bank > 0 && bank < 0x78) {
            SoundModuleInstrument.BankTag entry = findEntry(channel, seekPC, bank, -1);
            if (entry != null) {
                return entry;
            }
        }
        for (int i = 0x80; i >= 0x78; --i) {
            SoundModuleInstrument.BankTag entry = findEntry(channel, seekPC, i, -1);
            if (entry != null) {
                return entry;
            }
        }
        return null;
    }
}

コメント