• R/O
  • HTTP
  • SSH
  • HTTPS

MIDIChordHelper: Commit

Javaアプリ MIDI Chord Helper のソースコード


Commit MetaInfo

Revision73ad42e8ef6cfdcad295af501765c90268f6ad90 (tree)
Time2017-10-01 02:00:51
AuthorAkiyoshi Kamide <kamide@yk.r...>
CommiterAkiyoshi Kamide

Log Message

リファクタリング、エラー処理改善など

Change Summary

Incremental Difference

--- a/src/camidion/chordhelper/ChordHelperApplet.java
+++ b/src/camidion/chordhelper/ChordHelperApplet.java
@@ -13,7 +13,6 @@ import java.io.IOException;
1313 import java.net.URI;
1414 import java.net.URISyntaxException;
1515 import java.net.URL;
16-import java.nio.charset.Charset;
1716 import java.util.Arrays;
1817
1918 import javax.sound.midi.InvalidMidiDataException;
@@ -60,7 +59,6 @@ import camidion.chordhelper.midieditor.TempoSelecter;
6059 import camidion.chordhelper.midieditor.TimeSignatureSelecter;
6160 import camidion.chordhelper.music.Chord;
6261 import camidion.chordhelper.music.Key;
63-import camidion.chordhelper.music.MIDISpec;
6462 import camidion.chordhelper.music.Range;
6563 import camidion.chordhelper.pianokeyboard.MidiKeyboardPanel;
6664 import camidion.chordhelper.pianokeyboard.PianoKeyboardAdapter;
@@ -114,9 +112,7 @@ public class ChordHelperApplet extends JApplet {
114112 URL url = (new URI(midiFileUrl)).toURL();
115113 String filename = url.getFile().replaceFirst("^.*/","");
116114 Sequence sequence = MidiSystem.getSequence(url);
117- Charset charset = MIDISpec.getCharsetOf(sequence);
118- if( charset == null ) charset = Charset.defaultCharset();
119- int index = playlistModel.add(sequence, charset, filename);
115+ int index = playlistModel.add(sequence, filename);
120116 midiEditor.playlistTable.getSelectionModel().setSelectionInterval(index, index);
121117 return index;
122118 } catch( URISyntaxException|IOException|InvalidMidiDataException e ) {
@@ -144,10 +140,8 @@ public class ChordHelperApplet extends JApplet {
144140 */
145141 public int addToPlaylistBase64(String base64EncodedText, String filename) {
146142 Base64Dialog d = midiEditor.playlistTable.base64Dialog;
147- d.setBase64Data(base64EncodedText, filename);
148- int index = d.addToPlaylist();
149- midiEditor.playlistTable.getSelectionModel().setSelectionInterval(index, index);
150- return index;
143+ d.setBase64TextData(base64EncodedText, filename);
144+ return d.addToPlaylist();
151145 }
152146 /**
153147 * プレイリスト上で現在選択されているMIDIシーケンスをシーケンサへロードして再生します。
@@ -172,15 +166,13 @@ public class ChordHelperApplet extends JApplet {
172166 public boolean isPlaying() { return isRunning(); }
173167 /**
174168 * 現在シーケンサにロードされているMIDIデータをBase64テキストに変換した結果を返します。
175- * @return MIDIデータをBase64テキストに変換した結果(シーケンサにロードされていない場合null)
169+ * @return MIDIデータをBase64テキストに変換した結果
176170 * @throws IOException MIDIデータの読み込みに失敗した場合
177171 */
178172 public String getMidiDataBase64() throws IOException {
179- SequenceTrackListTableModel s = sequencerModel.getSequenceTrackListTableModel();
180- if( s == null ) return null;
181173 Base64Dialog d = midiEditor.playlistTable.base64Dialog;
182- d.setMIDIData(s.getMIDIdata());
183- return d.getBase64Data();
174+ d.setSequenceModel(sequencerModel.getSequenceTrackListTableModel());
175+ return d.getBase64TextData();
184176 }
185177 /**
186178 * 現在シーケンサにロードされているMIDIファイルのファイル名を返します。
@@ -274,7 +266,7 @@ public class ChordHelperApplet extends JApplet {
274266 */
275267 public static class VersionInfo {
276268 public static final String NAME = "MIDI Chord Helper";
277- public static final String VERSION = "Ver.20170722.1";
269+ public static final String VERSION = "Ver.20170930.1";
278270 public static final String COPYRIGHT = "Copyright (C) 2004-2017";
279271 public static final String AUTHER = "@きよし - Akiyoshi Kamide";
280272 public static final String URL = "http://www.yk.rim.or.jp/~kamide/music/chordhelper/";
@@ -288,9 +280,6 @@ public class ChordHelperApplet extends JApplet {
288280 /** ボタンの余白を詰めたいときに setMargin() の引数に指定するインセット */
289281 public static final Insets ZERO_INSETS = new Insets(0,0,0,0);
290282
291- // MIDIエディタダイアログ(Javaアプリメインからもアクセスできるようprivateにしていない)
292- MidiSequenceEditorDialog midiEditor;
293-
294283 // GUIコンポーネント(内部保存用)
295284 private PlaylistTableModel playlistModel;
296285 private MidiSequencerModel sequencerModel;
@@ -310,6 +299,15 @@ public class ChordHelperApplet extends JApplet {
310299 private JToggleButton anoGakkiToggleButton;
311300 private MidiDeviceTreeModel deviceTreeModel;
312301
302+ private MidiSequenceEditorDialog midiEditor;
303+ /**
304+ * MIDIエディタダイアログを返します。
305+ * @return MIDIエディタダイアログ
306+ */
307+ public MidiSequenceEditorDialog getMidiEditor() {
308+ return midiEditor;
309+ }
310+
313311 // アイコン画像
314312 private Image iconImage;
315313 public Image getIconImage() { return iconImage; }
@@ -412,7 +410,8 @@ public class ChordHelperApplet extends JApplet {
412410 });
413411 // 再生時間位置の移動、シーケンス名の変更、またはシーケンスの入れ替えが発生したときに呼び出されるリスナーを登録
414412 SongTitleLabel songTitleLabel = new SongTitleLabel();
415- sequencerModel.addChangeListener(e->{
413+ sequencerModel.addChangeListener(event->{
414+ MidiSequencerModel sequencerModel = (MidiSequencerModel) event.getSource();
416415 Sequencer sequencer = sequencerModel.getSequencer();
417416 chordMatrix.setPlaying(sequencer.isRunning());
418417 SequenceTrackListTableModel sequenceModel = sequencerModel.getSequenceTrackListTableModel();
@@ -457,8 +456,8 @@ public class ChordHelperApplet extends JApplet {
457456 add( Box.createHorizontalStrut(5) );
458457 add( darkModeToggleButton = new JToggleButton(new ButtonIcon(ButtonIcon.DARK_MODE_ICON)) {{
459458 setMargin(ZERO_INSETS);
460- addItemListener(e->{
461- boolean isDark = darkModeToggleButton.isSelected();
459+ addItemListener(event->{
460+ boolean isDark = ((JToggleButton)event.getSource()).isSelected();
462461 Color col = isDark ? Color.black : null;
463462 getContentPane().setBackground(isDark ? Color.black : rootPaneDefaultBgcolor);
464463 mainSplitPane.setBackground(col);
@@ -488,9 +487,9 @@ public class ChordHelperApplet extends JApplet {
488487 setMargin(ZERO_INSETS);
489488 setBorder(null);
490489 setToolTipText("あの楽器");
491- addItemListener(e->
490+ addItemListener(event->
492491 keyboardPanel.keyboardCenterPanel.keyboard.anoGakkiPane
493- = anoGakkiToggleButton.isSelected() ? anoGakkiPane : null
492+ = ((JToggleButton)event.getSource()).isSelected() ? anoGakkiPane : null
494493 );
495494 }} );
496495 add( Box.createHorizontalStrut(5) );
--- a/src/camidion/chordhelper/ChordTextField.java
+++ b/src/camidion/chordhelper/ChordTextField.java
@@ -104,7 +104,7 @@ public class ChordTextField extends JTextField {
104104 private Chord currentChord = null;
105105 /**
106106 * コードを追加します。
107- * @param chord コード
107+ * @param chord 追加するコード
108108 */
109109 public void appendChord(Chord chord) {
110110 if( currentChord == null && chord == null )
--- a/src/camidion/chordhelper/MidiChordHelper.java
+++ b/src/camidion/chordhelper/MidiChordHelper.java
@@ -25,9 +25,9 @@ import javax.swing.JLabel;
2525 import javax.swing.JOptionPane;
2626 import javax.swing.SwingUtilities;
2727 import javax.swing.WindowConstants;
28-import javax.swing.event.TableModelEvent;
2928
3029 import camidion.chordhelper.mididevice.MidiSequencerModel;
30+import camidion.chordhelper.midieditor.MidiSequenceEditorDialog;
3131 import camidion.chordhelper.midieditor.PlaylistTableModel;
3232 import camidion.chordhelper.midieditor.SequenceTrackListTableModel;
3333
@@ -41,7 +41,7 @@ public class MidiChordHelper extends JFrame implements AppletStub, AppletContext
4141 * @throws Exception 何らかの異常が発生した場合にスローされる
4242 */
4343 public static void main(String[] args) throws Exception {
44- List<File> fileList = Arrays.asList(args).stream()
44+ List<File> fileList = Arrays.stream(args)
4545 .map(arg -> new File(arg))
4646 .collect(Collectors.toList());
4747 SwingUtilities.invokeLater(()->new MidiChordHelper(fileList));
@@ -57,21 +57,17 @@ public class MidiChordHelper extends JFrame implements AppletStub, AppletContext
5757 private JLabel statusBar = new JLabel("Welcome to "+ChordHelperApplet.VersionInfo.NAME) {
5858 { setFont(getFont().deriveFont(Font.PLAIN)); }
5959 };
60- private void updateFilename(SequenceTrackListTableModel sequence) {
60+ private static String titleOf(SequenceTrackListTableModel sequence) {
6161 String title = ChordHelperApplet.VersionInfo.NAME;
6262 if( sequence != null ) {
6363 String filename = sequence.getFilename();
6464 if( filename != null && ! filename.isEmpty() )
6565 title = filename+" - "+title;
6666 }
67- setTitle(title);
67+ return title;
6868 }
69- private void updateFilename(MidiSequencerModel sequencer) {
70- updateFilename(sequencer.getSequenceTrackListTableModel());
71- }
72- private void updateFilename(TableModelEvent event) {
73- if( ! PlaylistTableModel.filenameChanged(event) ) return;
74- updateFilename(((PlaylistTableModel)event.getSource()).getSequencerModel());
69+ private void setTitleOf(MidiSequencerModel sequencer) {
70+ setTitle(titleOf(sequencer.getSequenceTrackListTableModel()));
7571 }
7672 private MidiChordHelper(List<File> fileList) {
7773 setTitle(ChordHelperApplet.VersionInfo.NAME);
@@ -103,13 +99,17 @@ public class MidiChordHelper extends JFrame implements AppletStub, AppletContext
10399 System.exit(0);
104100 }
105101 });
106- PlaylistTableModel playlist = applet.midiEditor.getPlaylistModel();
102+ MidiSequenceEditorDialog editor = applet.getMidiEditor();
103+ PlaylistTableModel playlist = editor.getPlaylistModel();
107104 MidiSequencerModel sequencer = playlist.getSequencerModel();
108- sequencer.addChangeListener(e->updateFilename((MidiSequencerModel)e.getSource()));
109- playlist.addTableModelListener(e->updateFilename(e));
110- updateFilename(sequencer);
105+ sequencer.addChangeListener(ce->setTitleOf(sequencer));
106+ playlist.addTableModelListener(tme->{
107+ if( playlist.isLoadedSequenceChanged(tme, PlaylistTableModel.Column.FILENAME) )
108+ setTitleOf(sequencer);
109+ });
110+ setTitleOf(sequencer);
111111 applet.start();
112- applet.midiEditor.play(fileList);
112+ editor.play(fileList);
113113 });
114114 }
115115 @Override
--- a/src/camidion/chordhelper/midieditor/Base64Dialog.java
+++ b/src/camidion/chordhelper/midieditor/Base64Dialog.java
@@ -4,8 +4,8 @@ import java.awt.event.ActionEvent;
44 import java.io.ByteArrayInputStream;
55 import java.io.IOException;
66 import java.io.InputStream;
7-import java.nio.charset.Charset;
87 import java.util.Base64;
8+import java.util.regex.Matcher;
99 import java.util.regex.Pattern;
1010
1111 import javax.sound.midi.InvalidMidiDataException;
@@ -21,52 +21,80 @@ import javax.swing.JOptionPane;
2121 import javax.swing.JPanel;
2222 import javax.swing.JScrollPane;
2323 import javax.swing.JTextArea;
24+import javax.swing.ListSelectionModel;
2425 import javax.swing.event.DocumentEvent;
2526 import javax.swing.event.DocumentListener;
2627
2728 import camidion.chordhelper.ButtonIcon;
2829 import camidion.chordhelper.ChordHelperApplet;
29-import camidion.chordhelper.music.MIDISpec;
3030
3131 /**
3232 * Base64テキスト入力ダイアログ
3333 */
34-public class Base64Dialog extends JDialog implements DocumentListener {
35- public static final Pattern HEADER_PATTERN = Pattern.compile("^.*:.*$", Pattern.MULTILINE);
34+public class Base64Dialog extends JDialog {
3635 private JTextArea base64TextArea = new JTextArea(8,56);
37- private void error(String message) {
36+ private PlaylistTable playlistTable;
37+ private void decodeError(String message) {
3838 JOptionPane.showMessageDialog(base64TextArea, (Object)message,
3939 ChordHelperApplet.VersionInfo.NAME, JOptionPane.WARNING_MESSAGE);
4040 base64TextArea.requestFocusInWindow();
4141 }
42- private String createHeader(String filename) {
43- return "Content-Type: audio/midi; name=\"" + filename + "\"\n"
44- + "Content-Transfer-Encoding: base64\n\n";
42+ private static String createHeaderOfFilename(String filename) {
43+ String header = "Content-Type: audio/midi; name=\"";
44+ if( filename != null ) header += filename;
45+ header += "\"\nContent-Transfer-Encoding: base64\n\n";
46+ return header;
47+ }
48+ private static String createBase64TextWithHeader(SequenceTrackListTableModel sequenceModel) throws IOException {
49+ if( sequenceModel == null ) return null;
50+ String text = createHeaderOfFilename(sequenceModel.getFilename());
51+ byte[] midiData = sequenceModel.getMIDIdata();
52+ if( midiData != null && midiData.length > 0 )
53+ text += Base64.getMimeEncoder().encodeToString(midiData);
54+ text += "\n";
55+ return text;
56+ }
57+ private static String bodyOf(String base64TextWithHeader) {
58+ // bodyには":"が含まれないのでヘッダと混同する心配なし
59+ return Pattern.compile("^.*:.*$", Pattern.MULTILINE).matcher(base64TextWithHeader).replaceAll("");
60+ }
61+ private static String filenameOf(String base64TextWithHeader) {
62+ Matcher m = Pattern.compile("(?i)^Content-Type:.*name=\"(.*)\"$", Pattern.MULTILINE).matcher(base64TextWithHeader);
63+ return m.find() ? m.group(1) : "";
4564 }
46- public PlaylistTable playlistTable;
4765 /**
4866 * 入力されたBase64テキストをデコードし、MIDIシーケンスとしてプレイリストに追加します。
4967 * @return プレイリストに追加されたMIDIシーケンスのインデックス(先頭が0)、追加に失敗した場合は -1
5068 */
5169 public int addToPlaylist() {
52- byte[] midiData = null;
70+ String base64Text = base64TextArea.getText();
71+ byte[] decodedData;
5372 try {
54- midiData = getMIDIData();
55- } catch(Exception e) {
56- error("Base64デコードに失敗しました。\n"+e);
73+ decodedData = Base64.getMimeDecoder().decode(bodyOf(base64Text).getBytes());
74+ } catch(Exception ex) {
75+ // 不正なBase64テキストが入力された場合
76+ decodeError("Base64デコードに失敗しました。\n"+ex);
5777 return -1;
5878 }
59- try (InputStream in = new ByteArrayInputStream(midiData)) {
60- Sequence sequence = MidiSystem.getSequence(in);
61- Charset charset = MIDISpec.getCharsetOf(sequence);
62- if( charset == null ) charset = Charset.defaultCharset();
63- int index = playlistTable.getModel().add(sequence, charset, null);
64- playlistTable.getSelectionModel().setSelectionInterval(index, index);
65- return index;
66- } catch( IOException|InvalidMidiDataException e ) {
67- error("Base64デコードした結果をMIDIシーケンスとして読み込めませんでした。\n"+e);
79+ Sequence sequence;
80+ try (InputStream in = new ByteArrayInputStream(decodedData)) {
81+ sequence = MidiSystem.getSequence(in);
82+ } catch( IOException|InvalidMidiDataException ex ) {
83+ // MIDI以外のデータをエンコードしたBase64テキストが入力された場合
84+ decodeError("Base64デコードした結果をMIDIシーケンスとして読み込めませんでした。\n"+ex);
85+ return -1;
86+ }
87+ int newIndex;
88+ try {
89+ newIndex = playlistTable.getModel().add(sequence, filenameOf(base64Text));
90+ } catch(Exception ex) {
91+ // 何らかの理由でプレイリストへの追加ができなかった場合
92+ decodeError("Base64デコードしたMIDIシーケンスをプレイリストに追加できませんでした。\n"+ex);
6893 return -1;
6994 }
95+ ListSelectionModel sm = playlistTable.getSelectionModel();
96+ if( sm != null ) sm.setSelectionInterval(newIndex, newIndex);
97+ return newIndex;
7098 }
7199 /**
72100 * Base64デコードアクション
@@ -84,8 +112,12 @@ public class Base64Dialog extends JDialog implements DocumentListener {
84112 public Action clearAction = new AbstractAction("Clear", new ButtonIcon(ButtonIcon.X_ICON)) {
85113 { putValue(Action.SHORT_DESCRIPTION, "Base64テキスト欄を消去"); }
86114 @Override
87- public void actionPerformed(ActionEvent e) { setText(null); }
115+ public void actionPerformed(ActionEvent e) { base64TextArea.setText(null); }
88116 };
117+ private void setActionEnabled(boolean b) {
118+ addBase64Action.setEnabled(b);
119+ clearAction.setEnabled(b);
120+ }
89121 /**
90122 * Base64テキスト入力ダイアログを構築します。
91123 * @param playlistTable Base64デコードされたMIDIシーケンスの追加先プレイリストビュー
@@ -105,73 +137,47 @@ public class Base64Dialog extends JDialog implements DocumentListener {
105137 }});
106138 setBounds( 300, 250, 660, 300 );
107139 base64TextArea.setToolTipText("Paste Base64-encoded MIDI sequence here");
108- base64TextArea.getDocument().addDocumentListener(this);
109- addBase64Action.setEnabled(false);
110- clearAction.setEnabled(false);
111- }
112- @Override
113- public void insertUpdate(DocumentEvent e) {
114- addBase64Action.setEnabled(true);
115- clearAction.setEnabled(true);
116- }
117- @Override
118- public void removeUpdate(DocumentEvent e) {
119- if( e.getDocument().getLength() > 0 ) return;
120- addBase64Action.setEnabled(false);
121- clearAction.setEnabled(false);
140+ base64TextArea.getDocument().addDocumentListener(new DocumentListener() {
141+ @Override
142+ public void insertUpdate(DocumentEvent e) {
143+ setActionEnabled(true);
144+ }
145+ @Override
146+ public void removeUpdate(DocumentEvent e) {
147+ if( e.getDocument().getLength() > 0 ) return;
148+ setActionEnabled(false);
149+ }
150+ @Override
151+ public void changedUpdate(DocumentEvent e) { }
152+ });
153+ setActionEnabled(false);
122154 }
123- @Override
124- public void changedUpdate(DocumentEvent e) { }
125155 /**
126- * バイナリー形式でMIDIデータを返します。
127- * @return バイナリー形式のMIDIデータ
128- * @throws IllegalArgumentException 入力されているテキストが有効なBase64スキームになっていない場合
156+ * MIDIシーケンスモデルを設定します。
157+ * @param sequenceModel MIDIシーケンスモデル
129158 */
130- public byte[] getMIDIData() {
131- String body = HEADER_PATTERN.matcher(base64TextArea.getText()).replaceAll("");
132- return Base64.getMimeDecoder().decode(body.getBytes());
133- }
134- /**
135- * バイナリー形式のMIDIデータを設定します。
136- * @param midiData バイナリー形式のMIDIデータ
137- */
138- public void setMIDIData(byte[] midiData) { setMIDIData(midiData, null); }
139- /**
140- * バイナリー形式のMIDIデータを、ファイル名をつけて設定します。
141- * @param midiData バイナリー形式のMIDIデータ
142- * @param filename ファイル名
143- */
144- public void setMIDIData(byte[] midiData, String filename) {
145- if( midiData == null || midiData.length == 0 ) return;
146- if( filename == null ) filename = "";
147- setText(createHeader(filename) + Base64.getMimeEncoder().encodeToString(midiData) + "\n");
159+ public void setSequenceModel(SequenceTrackListTableModel sequenceModel) {
160+ String text;
161+ try {
162+ text = createBase64TextWithHeader(sequenceModel);
163+ } catch (IOException ioex) {
164+ text = "File[" + sequenceModel.getFilename() + "]:" + ioex;
165+ }
166+ base64TextArea.setText(text);
148167 base64TextArea.selectAll();
149168 }
150169 /**
151- * Base64形式でMIDIデータを返します。
170+ * Base64形式でテキスト化されたMIDIデータを返します。
152171 * @return Base64形式のMIDIデータ
153172 */
154- public String getBase64Data() { return base64TextArea.getText(); }
173+ public String getBase64TextData() { return base64TextArea.getText(); }
155174 /**
156- * Base64形式のMIDIデータを設定します。
157- * @param base64Data Base64形式のMIDIデータ
175+ * Base64形式でテキスト化されたMIDIデータを、ヘッダつきで設定します。
176+ * @param base64TextData Base64形式のMIDIデータ
177+ * @param filename ヘッダに含めるファイル名(nullを指定すると""として設定される)
158178 */
159- public void setBase64Data(String base64Data) {
160- setText(null);
161- base64TextArea.append(base64Data);
179+ public void setBase64TextData(String base64TextData, String filename) {
180+ base64TextArea.setText(createHeaderOfFilename(filename));
181+ base64TextArea.append(base64TextData);
162182 }
163- /**
164- * Base64形式のMIDIデータを、ファイル名をつけて設定します。
165- * @param base64Data Base64形式のMIDIデータ
166- * @param filename ファイル名
167- */
168- public void setBase64Data(String base64Data, String filename) {
169- setText(createHeader(filename));
170- base64TextArea.append(base64Data);
171- }
172- /**
173- * テキスト文字列を設定します。
174- * @param text テキスト文字列
175- */
176- public void setText(String text) { base64TextArea.setText(text); }
177183 }
--- a/src/camidion/chordhelper/midieditor/PlaylistTable.java
+++ b/src/camidion/chordhelper/midieditor/PlaylistTable.java
@@ -44,7 +44,6 @@ import javax.swing.table.TableColumnModel;
4444
4545 import camidion.chordhelper.ChordHelperApplet;
4646 import camidion.chordhelper.mididevice.MidiSequencerModel;
47-import camidion.chordhelper.music.MIDISpec;
4847
4948 /**
5049 * プレイリストビュー(シーケンスリスト)
@@ -129,20 +128,7 @@ public class PlaylistTable extends JTable {
129128 }
130129 @Override
131130 public void actionPerformed(ActionEvent e) {
132- SequenceTrackListTableModel sequenceModel = getSelectedSequenceModel();
133- byte[] data = null;
134- String filename = null;
135- if( sequenceModel != null ) {
136- filename = sequenceModel.getFilename();
137- try {
138- data = sequenceModel.getMIDIdata();
139- } catch (IOException ioe) {
140- base64Dialog.setText("File["+filename+"]:"+ioe.toString());
141- base64Dialog.setVisible(true);
142- return;
143- }
144- }
145- base64Dialog.setMIDIData(data, filename);
131+ base64Dialog.setSequenceModel(getSelectedSequenceModel());
146132 base64Dialog.setVisible(true);
147133 }
148134 };
@@ -314,22 +300,28 @@ public class PlaylistTable extends JTable {
314300 */
315301 @Override
316302 public PlaylistTableModel getModel() { return (PlaylistTableModel)dataModel; }
303+ /**
304+ * {@link #add(List)} を呼び出し、このプレイリストにMIDIファイルを追加します。
305+ * @param files MIDIファイル
306+ * @return 追加されたMIDIファイルのインデックス値(先頭が0、追加されなかった場合は-1)
307+ */
308+ public int add(File... files) {
309+ return add(Arrays.asList(files));
310+ }
317311 /**
318312 * このプレイリストにMIDIファイルを追加します。追加に失敗した場合はダイアログを表示し、
319313 * 後続のMIDIファイルが残っていればそれを追加するかどうかをユーザに尋ねます。
320- * @param fileList MIDIファイルのリスト
314+ * @param files MIDIファイルのリスト
321315 * @return 追加されたMIDIファイルのインデックス値(先頭が0、追加されなかった場合は-1)
322316 */
323- public int add(List<File> fileList) {
317+ public int add(List<File> files) {
324318 int firstIndex = -1;
325- Iterator<File> itr = fileList.iterator();
319+ Iterator<File> itr = files.iterator();
326320 while(itr.hasNext()) {
327321 File file = itr.next();
328322 try (FileInputStream in = new FileInputStream(file)) {
329323 Sequence sequence = MidiSystem.getSequence(in);
330- Charset charset = MIDISpec.getCharsetOf(sequence);
331- if( charset == null ) charset = Charset.defaultCharset();
332- int lastIndex = ((PlaylistTableModel)dataModel).add(sequence, charset, file.getName());
324+ int lastIndex = ((PlaylistTableModel)dataModel).add(sequence, file.getName());
333325 if( firstIndex < 0 ) firstIndex = lastIndex;
334326 } catch(IOException|InvalidMidiDataException e) {
335327 String message = "Could not open as MIDI file "+file+"\n"+e;
@@ -458,7 +450,7 @@ public class PlaylistTable extends JTable {
458450 ex.printStackTrace();
459451 return;
460452 }
461- int firstIndex = PlaylistTable.this.add(Arrays.asList(getSelectedFile()));
453+ int firstIndex = PlaylistTable.this.add(getSelectedFile());
462454 try {
463455 PlaylistTableModel model = getModel();
464456 MidiSequencerModel sequencerModel = model.getSequencerModel();
--- a/src/camidion/chordhelper/midieditor/PlaylistTableModel.java
+++ b/src/camidion/chordhelper/midieditor/PlaylistTableModel.java
@@ -7,6 +7,7 @@ import java.util.HashMap;
77 import java.util.List;
88 import java.util.Map;
99 import java.util.Vector;
10+import java.util.stream.IntStream;
1011
1112 import javax.sound.midi.InvalidMidiDataException;
1213 import javax.sound.midi.Sequence;
@@ -21,7 +22,7 @@ import javax.swing.table.AbstractTableModel;
2122
2223 import camidion.chordhelper.ButtonIcon;
2324 import camidion.chordhelper.mididevice.MidiSequencerModel;
24-import camidion.chordhelper.music.ChordProgression;
25+import camidion.chordhelper.music.MIDISpec;
2526
2627 /**
2728 * プレイリスト(MIDIシーケンスリスト)のテーブルデータモデル
@@ -40,30 +41,18 @@ public class PlaylistTableModel extends AbstractTableModel {
4041 * 空のイベントリストモデル
4142 */
4243 public final MidiEventTableModel emptyEventListTableModel = new MidiEventTableModel(emptyTrackListTableModel, null);
43- /**
44- * テーブルモデルの変更を示すイベントが、ファイル名の変更によるものかどうかをチェックします。
45- * @param event テーブルモデルの変更を示すイベント
46- * @return ファイル名の変更による場合true
47- */
48- public static boolean filenameChanged(TableModelEvent event) {
49- int c = event.getColumn();
50- return c == Column.FILENAME.ordinal() || c == TableModelEvent.ALL_COLUMNS ;
51- }
5244 /** 再生中のシーケンサーの秒位置リスナー */
5345 private ChangeListener mmssPosition = new ChangeListener() {
5446 private int value = 0;
5547 @Override
5648 public void stateChanged(ChangeEvent event) {
5749 Object src = event.getSource();
58- if( src instanceof MidiSequencerModel ) {
59- MidiSequencerModel sequencerModel = (MidiSequencerModel)src;
60- int newValue = sequencerModel.getValue() / 1000;
61- if(value != newValue) {
62- value = newValue;
63- int rowIndex = sequenceModelList.indexOf(sequencerModel.getSequenceTrackListTableModel());
64- fireTableCellUpdated(rowIndex, Column.POSITION.ordinal());
65- }
66- }
50+ if( ! (src instanceof MidiSequencerModel) ) return;
51+ MidiSequencerModel sequencerModel = (MidiSequencerModel)src;
52+ int newValue = sequencerModel.getValue() / 1000;
53+ if(value == newValue) return;
54+ value = newValue;
55+ fireTableCellUpdated(sequencerModel.getSequenceTrackListTableModel(), Column.POSITION);
6756 }
6857 @Override
6958 public String toString() {
@@ -79,38 +68,35 @@ public class PlaylistTableModel extends AbstractTableModel {
7968 sequencerModel.addChangeListener(mmssPosition);
8069 sequencerModel.getSequencer().addMetaEventListener(msg->{
8170 // EOF(0x2F)が来て曲が終わったら次の曲へ進める
82- if(msg.getType() == 0x2F) SwingUtilities.invokeLater(()->{
83- try {
84- goNext();
85- } catch (InvalidMidiDataException e) {
86- throw new RuntimeException("Could not play next sequence after end-of-track",e);
87- }
88- });
71+ if(msg.getType() == 0x2F) SwingUtilities.invokeLater(()->goNext());
8972 });
9073 }
9174 /**
9275 * 次の曲へ進みます。
93- *
9476 * <p>リピートモードの場合は同じ曲をもう一度再生、そうでない場合は次の曲へ進んで再生します。
9577 * 次の曲がなければ、そこで停止します。いずれの場合も曲の先頭へ戻ります。
9678 * </p>
97- * @throws InvalidMidiDataException {@link Sequencer#setSequence(Sequence)} を参照
98- * @throws IllegalStateException MIDIシーケンサデバイスが閉じている場合
79+ * @throws IllegalStateException {@link #loadNext(int)} から
80+ * {@link InvalidMidiDataException} がスローされた場合
81+ * (MIDIシーケンサデバイスが閉じている状態で呼び出されたことが主な原因)
9982 */
100- private void goNext() throws InvalidMidiDataException {
83+ private void goNext() {
10184 // とりあえず曲の先頭へ戻る
10285 sequencerModel.getSequencer().setMicrosecondPosition(0);
103- if( (Boolean)toggleRepeatAction.getValue(Action.SELECTED_KEY) || loadNext(1) ) {
104- // リピートモードのときはもう一度同じ曲を、そうでない場合は次の曲を再生開始
105- sequencerModel.start();
106- }
107- else {
108- // 最後の曲が終わったので、停止状態にする
109- sequencerModel.stop();
110- // ここでボタンが停止状態に変わったはずなので、通常であれば再生ボタンが自力で再描画するところだが、
111- // セルのレンダラーが描く再生ボタンには効かないようなので、セルを突っついて再表示させる。
112- int rowIndex = sequenceModelList.indexOf(sequencerModel.getSequenceTrackListTableModel());
113- fireTableCellUpdated(rowIndex, Column.PLAY.ordinal());
86+ try {
87+ if( (Boolean)toggleRepeatAction.getValue(Action.SELECTED_KEY) || loadNext(1) ) {
88+ // リピートモードのときはもう一度同じ曲を、そうでない場合は次の曲を再生開始
89+ sequencerModel.start();
90+ }
91+ else {
92+ // 最後の曲が終わったので、停止状態にする
93+ sequencerModel.stop();
94+ // ここでボタンが停止状態に変わったはずなので、通常であれば再生ボタンが自力で再描画するところだが、
95+ // セルのレンダラーが描く再生ボタンには効かないようなので、セルを突っついて再表示させる。
96+ fireTableCellUpdated(sequencerModel.getSequenceTrackListTableModel(), Column.PLAY);
97+ }
98+ } catch (InvalidMidiDataException ex) {
99+ throw new IllegalStateException("Could not play next sequence after end-of-track",ex);
114100 }
115101 }
116102 /**
@@ -287,6 +273,46 @@ public class PlaylistTableModel extends AbstractTableModel {
287273 }
288274 public boolean isCellEditable() { return false; }
289275 public Object getValueOf(SequenceTrackListTableModel sequenceModel) { return ""; }
276+ /**
277+ * この列が変更されたか調べます。
278+ * @param event テーブルモデルの変更を示すイベント
279+ * @return この列に変更がある場合true
280+ */
281+ public boolean isChanged(TableModelEvent event) {
282+ int index = event.getColumn();
283+ return index == ordinal() || index == TableModelEvent.ALL_COLUMNS ;
284+ }
285+ }
286+ /**
287+ * 連携中のシーケンサにロードされているシーケンスの行の、指定された列が変更されたか調べます。
288+ * ロードされているシーケンスがない場合、変更なしとみなされます。
289+ * @param event テーブルモデルの変更を示すイベント
290+ * @param column 対象の列(nullを指定すると、どの列が変更されても、その行の変更だけで変更ありとみなされる)
291+ * @return 変更がある場合true
292+ */
293+ public boolean isLoadedSequenceChanged(TableModelEvent event, Column column) {
294+ if( column != null && ! column.isChanged(event) ) return false;
295+ SequenceTrackListTableModel loadedSequence = sequencerModel.getSequenceTrackListTableModel();
296+ return loadedSequence != null && IntStream.rangeClosed(event.getFirstRow(), event.getLastRow())
297+ .anyMatch( index -> index != TableModelEvent.HEADER_ROW && sequenceModelList.get(index) == loadedSequence );
298+ }
299+ /**
300+ * [row, column]にあるセルの値が更新されたことを、すべてのリスナーに通知します。
301+ * @param row 更新されたセルの行
302+ * @param column 更新されたセルの列
303+ * @see #fireTableCellUpdated(int, int)
304+ */
305+ public void fireTableCellUpdated(int row, Column column) {
306+ fireTableCellUpdated(row, column.ordinal());
307+ }
308+ /**
309+ * [sequence, column]にあるセルの値が更新されたことを、すべてのリスナーに通知します。
310+ * @param sequence 更新されたMIDIシーケンス
311+ * @param column 更新されたセルの列
312+ * @see #fireTableCellUpdated(int, int)
313+ */
314+ public void fireTableCellUpdated(SequenceTrackListTableModel sequence, Column column) {
315+ fireTableCellUpdated(sequenceModelList.indexOf(sequence), column);
290316 }
291317
292318 @Override
@@ -317,7 +343,7 @@ public class PlaylistTableModel extends AbstractTableModel {
317343 case NAME:
318344 // シーケンス名の設定または変更
319345 if( sequenceModelList.get(row).setName(val.toString()) )
320- fireTableCellUpdated(row, Column.MODIFIED.ordinal());
346+ fireTableCellUpdated(row, Column.MODIFIED);
321347 fireTableCellUpdated(row, column);
322348 break;
323349 case CHARSET:
@@ -326,7 +352,7 @@ public class PlaylistTableModel extends AbstractTableModel {
326352 seq.setCharset(Charset.forName(val.toString()));
327353 fireTableCellUpdated(row, column);
328354 // シーケンス名の表示更新
329- fireTableCellUpdated(row, Column.NAME.ordinal());
355+ fireTableCellUpdated(row, Column.NAME);
330356 // トラック名の表示更新
331357 seq.fireTableDataChanged();
332358 default:
@@ -342,20 +368,52 @@ public class PlaylistTableModel extends AbstractTableModel {
342368 return (int)(sequenceModelList.stream().mapToLong(m -> m.getMicrosecondLength() / 1000L).sum() / 1000L);
343369 }
344370 /**
345- * MIDIシーケンスを追加します。
346- * @param sequence MIDIシーケンス(nullの場合、シーケンスを自動生成して追加)
371+ * ファイル名なしでMIDIシーケンスを追加します。
372+ * 文字コードは自動的に判別されます(判別に失敗した場合はデフォルトの文字コードが指定されます)。
373+ * @param sequence MIDIシーケンス
374+ * @return 追加されたシーケンスのインデックス(先頭が 0)
375+ */
376+ public int add(Sequence sequence) {
377+ return add(sequence, (String)null);
378+ }
379+ /**
380+ * ファイル名を指定してMIDIシーケンスを追加します。
381+ * 文字コードは自動的に判別されます(判別に失敗した場合はデフォルトの文字コードが指定されます)。
382+ * @param sequence MIDIシーケンス
383+ * @param filename ファイル名(nullの場合、ファイル名なし)
384+ * @return 追加されたシーケンスのインデックス(先頭が 0)
385+ */
386+ public int add(Sequence sequence, String filename) {
387+ Charset charset = MIDISpec.getCharsetOf(sequence);
388+ if( charset == null ) charset = Charset.defaultCharset();
389+ return add(sequence, charset, filename);
390+ }
391+ /**
392+ * ファイル名なしで、文字コードを指定してMIDIシーケンスを追加します。
393+ * @param sequence MIDIシーケンス
394+ * @param charset MIDIシーケンス内のテキスト文字コード
395+ * @return 追加されたシーケンスのインデックス(先頭が 0)
396+ */
397+ public int add(Sequence sequence, Charset charset) {
398+ return add(sequence, charset, null);
399+ }
400+ /**
401+ * ファイル名、文字コードを指定してMIDIシーケンスを追加します。
402+ * @param sequence MIDIシーケンス
347403 * @param charset MIDIシーケンス内のテキスト文字コード
348404 * @param filename ファイル名(nullの場合、ファイル名なし)
349405 * @return 追加されたシーケンスのインデックス(先頭が 0)
350406 */
351407 public int add(Sequence sequence, Charset charset, String filename) {
352- if( sequence == null ) {
353- sequence = (new ChordProgression()).toMidiSequence(charset);
354- }
355- sequenceModelList.add(new SequenceTrackListTableModel(this, sequence, charset, filename));
356- int lastIndex = sequenceModelList.size() - 1;
357- fireTableRowsInserted(lastIndex, lastIndex);
358- return lastIndex;
408+ //if( sequence == null ) {
409+ // sequence = (new ChordProgression()).toMidiSequence(charset);
410+ //}
411+ SequenceTrackListTableModel sequenceModel =
412+ new SequenceTrackListTableModel(this, sequence, charset, filename);
413+ int newIndex = sequenceModelList.size();
414+ sequenceModelList.add(sequenceModel);
415+ fireTableRowsInserted(newIndex, newIndex);
416+ return newIndex;
359417 }
360418 /**
361419 * MIDIシーケンスを除去します。除去されたMIDIシーケンスがシーケンサーにロード済みだった場合、アンロードします。
@@ -383,16 +441,13 @@ public class PlaylistTableModel extends AbstractTableModel {
383441 SequenceTrackListTableModel oldSeq = sequencerModel.getSequenceTrackListTableModel();
384442 SequenceTrackListTableModel newSeq = (newRowIndex < 0 || sequenceModelList.isEmpty() ? null : sequenceModelList.get(newRowIndex));
385443 if( ! sequencerModel.setSequenceTrackListTableModel(newSeq) ) return;
386- int columnIndices[] = {
387- Column.PLAY.ordinal(),
388- Column.POSITION.ordinal(),
389- };
390444 if( oldSeq != null ) {
391- int oldRowIndex = sequenceModelList.indexOf(oldSeq);
392- for( int columnIndex : columnIndices ) fireTableCellUpdated(oldRowIndex, columnIndex);
445+ fireTableCellUpdated(oldSeq, Column.PLAY);
446+ fireTableCellUpdated(oldSeq, Column.POSITION);
393447 }
394448 if( newSeq != null ) {
395- for( int columnIndex : columnIndices ) fireTableCellUpdated(newRowIndex, columnIndex);
449+ fireTableCellUpdated(newRowIndex, Column.PLAY);
450+ fireTableCellUpdated(newRowIndex, Column.POSITION);
396451 }
397452 }
398453 /**
@@ -414,7 +469,7 @@ public class PlaylistTableModel extends AbstractTableModel {
414469 * @throws IllegalStateException MIDIシーケンサデバイスが閉じている場合
415470 */
416471 public int play(Sequence sequence, Charset charset) throws InvalidMidiDataException {
417- int lastIndex = add(sequence, charset, "");
472+ int lastIndex = add(sequence, charset);
418473 if( ! sequencerModel.getSequencer().isRunning() ) play(lastIndex);
419474 return lastIndex;
420475 }
--- a/src/camidion/chordhelper/midieditor/SequenceTrackListTableModel.java
+++ b/src/camidion/chordhelper/midieditor/SequenceTrackListTableModel.java
@@ -5,6 +5,7 @@ import java.io.IOException;
55 import java.nio.charset.Charset;
66 import java.util.ArrayList;
77 import java.util.List;
8+import java.util.stream.Stream;
89
910 import javax.sound.midi.MidiSystem;
1011 import javax.sound.midi.Sequence;
@@ -47,6 +48,15 @@ public class SequenceTrackListTableModel extends AbstractTableModel {
4748 }
4849 }
4950 /**
51+ * [row, column]にあるセルの値が更新されたことを、すべてのリスナーに通知します。
52+ * @param row 更新されたセルの行
53+ * @param column 更新されたセルの列
54+ * @see #fireTableCellUpdated(int, int)
55+ */
56+ public void fireTableCellUpdated(int row, Column column) {
57+ fireTableCellUpdated(row, column.ordinal());
58+ }
59+ /**
5060 * このモデルを収容している親のプレイリストを返します。
5161 */
5262 public PlaylistTableModel getParent() { return sequenceListTableModel; }
@@ -103,6 +113,13 @@ public class SequenceTrackListTableModel extends AbstractTableModel {
103113 public int getRowCount() {
104114 return sequence == null ? 0 : sequence.getTracks().length;
105115 }
116+ /**
117+ * トラックが存在しない、空のシーケンスかどうか調べます。
118+ * @return トラックが存在しなければtrue
119+ */
120+ public boolean isEmpty() {
121+ return sequence == null || sequence.getTracks().length == 0;
122+ }
106123 @Override
107124 public int getColumnCount() { return Column.values().length; }
108125 /**
@@ -193,7 +210,7 @@ public class SequenceTrackListTableModel extends AbstractTableModel {
193210 if( ch == trackTableModel.getChannel() ) break;
194211 trackTableModel.setChannel(ch);
195212 setModified(true);
196- fireTableCellUpdated(row, Column.EVENTS.ordinal());
213+ fireTableCellUpdated(row, Column.EVENTS);
197214 break;
198215 }
199216 case TRACK_NAME: trackModelList.get(row).setString((String)val); break;
@@ -222,6 +239,14 @@ public class SequenceTrackListTableModel extends AbstractTableModel {
222239 */
223240 public SequenceTickIndex getSequenceTickIndex() { return sequenceTickIndex; }
224241 /**
242+ * トラックから子モデルを生成します。
243+ * @param track 対象トラック
244+ * @return 対象トラックから生成した子モデル
245+ */
246+ private MidiEventTableModel createModelOf(Track track) {
247+ return new MidiEventTableModel(this, track);
248+ }
249+ /**
225250 * MIDIシーケンスを設定します。
226251 * @param sequence MIDIシーケンス(nullを指定するとトラックリストが空になる)
227252 */
@@ -246,11 +271,12 @@ public class SequenceTrackListTableModel extends AbstractTableModel {
246271 fireTimeSignatureChanged();
247272 //
248273 // トラックリストを再構築
249- Track tracks[] = sequence.getTracks();
250- for(Track track : tracks) trackModelList.add(new MidiEventTableModel(this, track));
274+ Track[] tracks = sequence.getTracks();
275+ int newSize = tracks.length;
276+ Stream.of(tracks).forEach(track -> trackModelList.add(createModelOf(track)));
251277 //
252278 // トラックが挿入されたことを通知
253- fireTableRowsInserted(0, tracks.length-1);
279+ fireTableRowsInserted(0, newSize-1);
254280 }
255281 /**
256282 * 拍子が変更されたとき、シーケンスtickインデックスを再作成します。
@@ -289,8 +315,8 @@ public class SequenceTrackListTableModel extends AbstractTableModel {
289315 * @return 成功したらtrue
290316 */
291317 public boolean setName(String name) {
292- if( name.equals(toString()) || sequence == null ) return false;
293- if( ! MIDISpec.setNameBytesOf(sequence, name.getBytes(charset)) ) return false;
318+ if( name.equals(toString()) || ! MIDISpec.setNameBytesOf(sequence, name.getBytes(charset)) )
319+ return false;
294320 setModified(true);
295321 fireTableDataChanged();
296322 if( isOnSequencer() )
@@ -303,9 +329,7 @@ public class SequenceTrackListTableModel extends AbstractTableModel {
303329 * @throws IOException バイト列の出力に失敗した場合
304330 */
305331 public byte[] getMIDIdata() throws IOException {
306- if( sequence == null || sequence.getTracks().length == 0 ) {
307- return null;
308- }
332+ if( isEmpty() ) return null;
309333 try( ByteArrayOutputStream out = new ByteArrayOutputStream() ) {
310334 MidiSystem.write(sequence, 1, out);
311335 return out.toByteArray();
@@ -329,10 +353,8 @@ public class SequenceTrackListTableModel extends AbstractTableModel {
329353 * @return トラックモデル(見つからない場合null)
330354 */
331355 public MidiEventTableModel getSelectedTrackModel(ListSelectionModel selectionModel) {
332- if( sequence == null || selectionModel.isSelectionEmpty() ) return null;
333- Track tracks[] = sequence.getTracks();
334- if( tracks.length == 0 ) return null;
335- Track t = tracks[selectionModel.getMinSelectionIndex()];
356+ if( isEmpty() || selectionModel.isSelectionEmpty() ) return null;
357+ Track t = sequence.getTracks()[selectionModel.getMinSelectionIndex()];
336358 return trackModelList.stream().filter(tm -> tm.getTrack() == t).findFirst().orElse(null);
337359 }
338360 /**
@@ -352,9 +374,9 @@ public class SequenceTrackListTableModel extends AbstractTableModel {
352374 * @return 追加したトラックのインデックス(先頭 0)
353375 */
354376 public int createTrack() {
377+ int newIndex = getRowCount();
355378 trackModelList.add(new MidiEventTableModel(this, sequence.createTrack()));
356379 setModified(true);
357- int newIndex = getRowCount() - 1;
358380 fireTableRowsInserted(newIndex, newIndex);
359381 return newIndex;
360382 }
--- a/src/camidion/chordhelper/music/MIDISpec.java
+++ b/src/camidion/chordhelper/music/MIDISpec.java
@@ -182,11 +182,11 @@ public class MIDISpec {
182182 */
183183 public static byte[] getNameBytesOf(Sequence sequence) {
184184 return Arrays.stream(sequence.getTracks()).map(t->getNameBytesOf(t))
185- .filter(Objects::nonNull).findFirst().orElse(null);
185+ .filter(Objects::nonNull).findFirst().orElse(null);
186186 }
187187 /**
188188 * シーケンス名のバイト列を設定します。
189- * <p>先頭のトラックに設定されます。設定に失敗した場合、順に次のトラックへの設定を試みます。
189+ * <p>先頭のトラックから順に設定を試み、成功したところでtrueを返します。
190190 * </p>
191191 *
192192 * @param sequence MIDIシーケンス
@@ -194,7 +194,8 @@ public class MIDISpec {
194194 * @return 成功:true、失敗:false
195195 */
196196 public static boolean setNameBytesOf(Sequence sequence, byte[] name) {
197- return Arrays.stream(sequence.getTracks()).anyMatch(t->setNameBytesOf(t,name));
197+ return sequence != null && Arrays.stream(sequence.getTracks())
198+ .anyMatch(t->setNameBytesOf(t,name));
198199 }
199200 /**
200201 * 指定されたMIDIシーケンスからメタイベントのテキスト(名前や歌詞など)を検索し、その文字コードを判定します。
Show on old repository browser