import javax.swing.JApplet;
import javax.swing.JButton;
import javax.swing.JComponent;
-import javax.swing.JLabel;
import javax.swing.JLayeredPane;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
* @param isDark ダークモードのときtrue、明るい表示のときfalse(デフォルト)
*/
public void setDarkMode(boolean isDark) {
- darkModeToggleButton.setSelected(isDark);
+ if( darkModeToggleButton.isSelected() != isDark )
+ darkModeToggleButton.doClick();
}
/**
* バージョン情報
*/
public static class VersionInfo {
public static final String NAME = "MIDI Chord Helper";
- public static final String VERSION = "Ver.20170523.1";
+ public static final String VERSION = "Ver.20170722.1";
public static final String COPYRIGHT = "Copyright (C) 2004-2017";
public static final String AUTHER = "@きよし - Akiyoshi Kamide";
public static final String URL = "http://www.yk.rim.or.jp/~kamide/music/chordhelper/";
private ChordMatrix chordMatrix;
private JPanel keyboardSequencerPanel;
private JPanel chordGuide;
- private Color rootPaneDefaultBgcolor;
- private Color lyricDisplayDefaultBgcolor;
- private Border lyricDisplayDefaultBorder;
private JSplitPane mainSplitPane;
private JSplitPane keyboardSplitPane;
private ChordButtonLabel enterButtonLabel;
AboutMessagePane about = new AboutMessagePane(imageIcon);
//
// 背景色の取得
- rootPaneDefaultBgcolor = getContentPane().getBackground();
+ Color rootPaneDefaultBgcolor = getContentPane().getBackground();
//
// コードダイアグラム、コードボタン、ピアノ鍵盤、およびそれらの仮想MIDIデバイスを生成
CapoComboBoxModel capoComboBoxModel = new CapoComboBoxModel();
}
{
addChordMatrixListener(new ChordMatrixListener(){
+ @Override
public void keySignatureChanged() {
keyboardPanel.setCapoKey(getKeySignatureCapo());
}
+ @Override
public void chordChanged() { chordOn(); }
});
capoSelecter.checkbox.addItemListener(e->clearChord());
(lyricDisplay = new ChordTextField(sequencerModel)).addActionListener(
e->chordMatrix.setSelectedChord(e.getActionCommand().trim().split("[ \t\r\n]")[0])
);
- lyricDisplayDefaultBorder = lyricDisplay.getBorder();
- lyricDisplayDefaultBgcolor = lyricDisplay.getBackground();
+ Border lyricDisplayDefaultBorder = lyricDisplay.getBorder();
+ Color lyricDisplayDefaultBgcolor = lyricDisplay.getBackground();
//
// メタイベント(テンポ・拍子・調号)を受信して表示するリスナーを登録
TempoSelecter tempoSelecter = new TempoSelecter() {{ setEditable(false); }};
}
});
// 再生時間位置の移動、シーケンス名の変更、またはシーケンスの入れ替えが発生したときに呼び出されるリスナーを登録
- JLabel songTitleLabel = new JLabel();
+ SongTitleLabel songTitleLabel = new SongTitleLabel();
sequencerModel.addChangeListener(e->{
Sequencer sequencer = sequencerModel.getSequencer();
chordMatrix.setPlaying(sequencer.isRunning());
SequenceTrackListTableModel sequenceModel = sequencerModel.getSequenceTrackListTableModel();
if( sequenceModel == null ) {
- songTitleLabel.setText("<html>[No MIDI file loaded]</html>");
+ songTitleLabel.clear();
timesigSelecter.clear();
tempoSelecter.clear();
keysigLabel.clear();
return;
}
- String songTitle = sequenceModel.toString();
int songIndex = playlistModel.getSequenceModelList().indexOf(sequenceModel);
- songTitleLabel.setText("<html>"+("MIDI file " + songIndex + ": " + (
- songTitle.isEmpty() ? "[Untitled]" : "<font color=maroon>"+songTitle+"</font>"
- ))+"</html>");
+ songTitleLabel.setSongTitle(songIndex, sequenceModel);
SequenceTickIndex tickIndex = sequenceModel.getSequenceTickIndex();
long tickPosition = sequencer.getTickPosition();
tickIndex.tickToMeasure(tickPosition);
chordMatrix.setBeat(tickIndex);
if( sequencerModel.getValueIsAdjusting() || ! (sequencer.isRunning() || sequencer.isRecording()) ) {
- MetaMessage msg;
- msg = tickIndex.lastMetaMessageAt(
- SequenceTickIndex.MetaMessageType.TIME_SIGNATURE, tickPosition
- );
- timesigSelecter.setValue(msg==null ? null : msg.getData());
- msg = tickIndex.lastMetaMessageAt(
- SequenceTickIndex.MetaMessageType.TEMPO, tickPosition
- );
- tempoSelecter.setTempo(msg==null ? null : msg.getData());
- msg = tickIndex.lastMetaMessageAt(
- SequenceTickIndex.MetaMessageType.KEY_SIGNATURE, tickPosition
- );
- if(msg == null) keysigLabel.clear(); else setKeySignature(new Key(msg.getData()));
+ timesigSelecter.setValueAt(tickIndex, tickPosition);
+ tempoSelecter.setTempoAt(tickIndex, tickPosition);
+ setKeySignatureAt(tickIndex, tickPosition);
}
});
sequencerModel.fireStateChanged();
addMouseListener(new MouseAdapter() {
public void mousePressed(MouseEvent event) {
boolean rightClicked = (event.getModifiersEx() & InputEvent.BUTTON3_DOWN_MASK) != 0;
- if( rightClicked )
- chordMatrix.setSelectedChord((Chord)null);
- else
- chordMatrix.setSelectedChord(lyricDisplay.getText());
+ String selectedChord = rightClicked ? lyricDisplay.getText() : null;
+ chordMatrix.setSelectedChord(selectedChord);
}
});
}});
add( Box.createHorizontalStrut(5) );
add( darkModeToggleButton = new JToggleButton(new ButtonIcon(ButtonIcon.DARK_MODE_ICON)) {{
setMargin(ZERO_INSETS);
- addItemListener(e->innerSetDarkMode(darkModeToggleButton.isSelected()));
+ addItemListener(e->{
+ boolean isDark = darkModeToggleButton.isSelected();
+ Color col = isDark ? Color.black : null;
+ getContentPane().setBackground(isDark ? Color.black : rootPaneDefaultBgcolor);
+ mainSplitPane.setBackground(col);
+ keyboardSplitPane.setBackground(col);
+ enterButtonLabel.setDarkMode(isDark);
+ chordGuide.setBackground(col);
+ lyricDisplay.setBorder(isDark ? null : lyricDisplayDefaultBorder);
+ lyricDisplay.setBackground(isDark ?
+ chordMatrix.darkModeColorset.backgrounds[2] :
+ lyricDisplayDefaultBgcolor
+ );
+ lyricDisplay.setForeground(isDark ? Color.white : null);
+ inversionOmissionButton.setBackground(col);
+ anoGakkiToggleButton.setBackground(col);
+ keyboardSequencerPanel.setBackground(col);
+ chordDiagram.setBackground(col);
+ chordDiagram.titleLabel.setDarkMode(isDark);
+ chordMatrix.setDarkMode(isDark);
+ keyboardPanel.setDarkMode(isDark);
+ });
setToolTipText("Light / Dark - 明かりを点灯/消灯");
setBorder(null);
}});
@Override
public void stop() { sequencerModel.stop(); }
- private void innerSetDarkMode(boolean isDark) {
- Color col = isDark ? Color.black : null;
- getContentPane().setBackground(isDark ? Color.black : rootPaneDefaultBgcolor);
- mainSplitPane.setBackground(col);
- keyboardSplitPane.setBackground(col);
- enterButtonLabel.setDarkMode(isDark);
- chordGuide.setBackground(col);
- lyricDisplay.setBorder(isDark ? null : lyricDisplayDefaultBorder);
- lyricDisplay.setBackground(isDark ?
- chordMatrix.darkModeColorset.backgrounds[2] :
- lyricDisplayDefaultBgcolor
- );
- lyricDisplay.setForeground(isDark ? Color.white : null);
- inversionOmissionButton.setBackground(col);
- anoGakkiToggleButton.setBackground(col);
- keyboardSequencerPanel.setBackground(col);
- chordDiagram.setBackground(col);
- chordDiagram.titleLabel.setDarkMode(isDark);
- chordMatrix.setDarkMode(isDark);
- keyboardPanel.setDarkMode(isDark);
- }
-
private void setKeySignature(Key key) {
keysigLabel.setKey(key);
chordMatrix.setKeySignature(key);
}
+ private void setKeySignatureAt(SequenceTickIndex tickIndex, long tickPosition) {
+ MetaMessage msg = tickIndex.lastMetaMessageAt(
+ SequenceTickIndex.MetaMessageType.KEY_SIGNATURE,
+ tickPosition
+ );
+ if(msg == null) keysigLabel.clear(); else setKeySignature(new Key(msg.getData()));
+ }
private int[] chordOnNotes = null;
/**
--- /dev/null
+package camidion.chordhelper;
+
+import javax.swing.JLabel;
+
+import camidion.chordhelper.midieditor.SequenceTrackListTableModel;
+
+public class SongTitleLabel extends JLabel {
+ public void clear() {
+ setText("<html>[No MIDI file loaded]</html>");
+ }
+ public void setSongTitle(int songIndex, SequenceTrackListTableModel sequenceModel) {
+ String title = sequenceModel.toString();
+ String titleHtml = title.isEmpty()?"[Untitled]":"<font color=maroon>"+title+"</font>";
+ setText("<html>MIDI file " + songIndex + ": " + titleHtml + "</html>");
+ }
+}
package camidion.chordhelper.mididevice;
-import java.util.AbstractCollection;
import java.util.AbstractList;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Objects;
-import java.util.Vector;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public String toString() { return "MIDI devices"; }
protected List<MidiDeviceModel> deviceModelList = new ArrayList<>();
+
@Override
public int size() { return deviceModelList.size(); }
@Override
protected Map<MidiDeviceInOutType, List<MidiDeviceModel>> deviceModelTree
= MidiDeviceInOutType.stream().collect(Collectors.toMap(Function.identity(), t-> new ArrayList<>()));
- /**
- * {@link AbstractList#add(E)}の操作を内部的に行います。
- * 指定された要素をこのリストの最後に追加し、ツリー構造にも反映します。
- * @param dm 追加するMIDIデバイスモデル
- * @return true({@link AbstractList#add(E)} と同様)
- */
- protected boolean addInternally(MidiDeviceModel dm) {
- if( ! deviceModelList.add(dm) ) return false;
- deviceModelTree.get(dm.getInOutType()).add(dm);
- return true;
- }
+
protected MidiDeviceModel add(MidiDevice device) {
if( device == null ) return null;
- MidiDeviceModel m = new MidiDeviceModel(device,this);
- addInternally(m);
- return m;
+ MidiDeviceModel dm;
+ if( device instanceof Sequencer ) {
+ dm = new MidiSequencerModel((Sequencer)device,this);
+ } else {
+ dm = new MidiDeviceModel(device,this);
+ }
+ deviceModelList.add(dm);
+ deviceModelTree.get(dm.getInOutType()).add(dm);
+ return dm;
}
protected MidiSequencerModel add(Sequencer sequencer) {
- if( sequencer == null ) return null;
- MidiSequencerModel m = new MidiSequencerModel(sequencer,this);
- addInternally(m);
- return m;
- }
- /**
- * {@link AbstractCollection#removeAll(Collection)}の操作を内部的に行います。
- * 指定されたコレクションに該当するすべての要素を、このリストから削除します。
- * このリストが変更された場合、ツリー構造にも反映されます。
- * @param c 削除する要素のコレクション
- * @return このリストが変更された場合はtrue({@link AbstractCollection#removeAll(Collection)} と同様)
- */
- protected boolean removeAllInternally(Collection<?> c) {
- if( ! deviceModelList.removeAll(c) ) return false;
- c.stream().filter(o -> o instanceof MidiDeviceModel).forEach(
- o -> deviceModelTree.get(((MidiDeviceModel)o).getInOutType()).remove(o)
- );
- return true;
+ return (MidiSequencerModel)add((MidiDevice)sequencer);
}
@Override
public Object getRoot() { return this; }
// NOTE: 必ず MIDI OUT Rx デバイスを先に開くこと。
//
// そうすれば、後から開いた MIDI IN Tx デバイスからのタイムスタンプのほうが「若く」なるので、
- // 相手の MIDI OUT Rx デバイスは「信号が遅れてやってきた」と認識、遅れを取り戻そうとして
- // 即座に音を出してくれる。
+ // 相手の MIDI OUT Rx デバイスは「若いころの信号が遅れてやってきた」と認識、
+ // 遅れを取り戻そうとして即座に音を出してくれる。
//
// 開く順序が逆になると「進みすぎるから遅らせよう」として無用なレイテンシーが発生する原因になる。
synthModel, firstMidiOutModel, // MIDI OUT Rx
// 自身のTx/Rx同士の接続は、シーケンサーモデルはなし、それ以外(GUIデバイスモデル)はあり。
connectDevices(
openedDeviceModels.stream().filter(
- m->Objects.nonNull(m.getReceiverListModel())
+ rxm->Objects.nonNull(rxm.getReceiverListModel())
).collect(Collectors.toMap(
Function.identity(),
- r->openedDeviceModels.stream()
- .filter(m->Objects.nonNull(m.getTransmitterListModel()))
- .filter(t-> !(t == sequencerModel && t == r))
+ rxm->openedDeviceModels.stream()
+ .filter(txm->Objects.nonNull(txm.getTransmitterListModel()))
+ .filter(txm-> !(txm == sequencerModel && txm == rxm))
.collect(Collectors.toList())
))
);
* </ul>
*/
private void connectDevices(Map<MidiDeviceModel, Collection<MidiDeviceModel>> rxToTxConnections) {
- rxToTxConnections.keySet().stream().forEach(rxm->{
+ rxToTxConnections.entrySet().stream().forEach(rxe->{
+ MidiDeviceModel rxm = rxe.getKey();
Receiver rx = rxm.getReceiver();
if( rx == null ) return;
- rxToTxConnections.get(rxm).stream().filter(Objects::nonNull).forEach(txm->{
+ rxe.getValue().stream().filter(Objects::nonNull).forEach(txm->{
try {
txm.getTransmitterListModel().openTransmitter().setReceiver(rx);
} catch( Exception ex ) {
* 新しく装着されたMIDIデバイスを開くことができるようになります。
* 同時に、取り外されたMIDIデバイスが閉じられ、そのデバイスの表示も消えます。
* </p>
- * <p>別のMIDIデバイスに
+ * <p>USB端子からMIDIデバイスを抜いた場合、そのMIDIデバイスの
* {@link Transmitter} や {@link Receiver}
- * を接続したままUSB端子からMIDIデバイスを抜くと、
- * {@link MidiSystem#getMidiDeviceInfo()} ã\82\92å\91¼ã\81³å\87ºã\81\97ã\81\9fã\81¨ã\81\8dã\81« Java VM ã\81\8cã\82¯ã\83©ã\83\83ã\82·ã\83¥ã\81\97ã\81¦ã\81\97ã\81¾ã\81\86ã\81\9fã\82\81ã\80\81
- * 最初に{@link #disconnectAllDevices()}で接続をすべて切断してから
+ * を接続したままで
+ * {@link MidiSystem#getMidiDeviceInfo()} ã\82\92å\91¼ã\81³å\87ºã\81\99ã\81¨ Java VM ã\81\8cã\82¯ã\83©ã\83\83ã\82·ã\83¥ã\81\97ã\81¦ã\81\97ã\81¾ã\81\84ã\81¾ã\81\99ã\80\82
+ * これを避けるため、最初に{@link #disconnectAllDevices()}で接続をすべて切断してから
* MIDIデバイスリストを最新の状態に更新し、その後{@link #connectDevices(Map)}で接続を復元します。
* </p>
*/
public void update() {
- Map<MidiDeviceModel, Collection<MidiDeviceModel>> savedConnections = disconnectAllDevices();
- List<MidiDevice.Info> additionalInfo = new Vector<>(getMidiDeviceInfo());
- List<MidiDeviceModel> closedModels = stream().filter(model->{
+ Map<MidiDeviceModel, Collection<MidiDeviceModel>> saved = disconnectAllDevices();
+ List<MidiDevice.Info> newDeviceInfo = new ArrayList<>(getMidiDeviceInfo());
+ Collection<MidiDeviceModel> oldDeviceModels = stream().filter(model->{
MidiDevice device = model.getMidiDevice();
if( device instanceof VirtualMidiDevice ) return false;
- if( additionalInfo.remove(device.getDeviceInfo()) ) return false;
+ if( newDeviceInfo.remove(device.getDeviceInfo()) ) return false;
device.close(); return true;
- }).collect(Collectors.toList());
- if( removeAllInternally(closedModels) ) {
- savedConnections.keySet().removeAll(closedModels); // Rx
- savedConnections.values().forEach(m->m.removeAll(closedModels)); // Tx
+ }).collect(Collectors.toSet());
+ if( deviceModelList.removeAll(oldDeviceModels) ) {
+ oldDeviceModels.forEach(m->deviceModelTree.get(m.getInOutType()).remove(m));
+ saved.keySet().removeAll(oldDeviceModels); // Rx
+ saved.values().forEach(m->m.removeAll(oldDeviceModels)); // Tx
}
- additionalInfo.forEach(info->add(getMidiDevice(info)));
- connectDevices(savedConnections);
+ newDeviceInfo.forEach(info->add(getMidiDevice(info)));
+ connectDevices(saved);
fireTreeStructureChanged(this, null, null, null);
}
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
+import javax.swing.JSeparator;
import javax.swing.JSpinner;
import javax.swing.JTabbedPane;
import javax.swing.JTextArea;
add(ppqComboBox);
add(measureSelecter);
}});
- add(new JButton("Randomize (Tempo, Time signature, Chord progression)") {{
- setMargin(ChordHelperApplet.ZERO_INSETS);
- addActionListener(e->setRandomChordProgression(measureSelecter.getMeasureDuration()));
- }});
+ add(new JSeparator());
add(new JPanel() {{
setLayout(new BoxLayout(this, BoxLayout.LINE_AXIS));
+ add(new JButton("Randomize") {{
+ setMargin(ChordHelperApplet.ZERO_INSETS);
+ addActionListener(e->setRandomChordProgression(measureSelecter.getMeasureDuration()));
+ }});
add(tempoSelecter);
add(new JPanel() {{
add(new JLabel("Time signature ="));
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
+import javax.sound.midi.MetaMessage;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JLabel;
public void setTempo(byte msgdata[]) {
if( msgdata==null ) clear(); else setTempo(MIDISpec.byteArrayToQpmTempo(msgdata));
}
+ /**
+ * MIDIシーケンスの特定tick位置におけるテンポを設定します。
+ * @param tickIndex MIDIシーケンスのtickインデックス
+ * @param tickPosition tick位置
+ */
+ public void setTempoAt(SequenceTickIndex tickIndex, long tickPosition) {
+ MetaMessage msg = tickIndex.lastMetaMessageAt(
+ SequenceTickIndex.MetaMessageType.TEMPO,
+ tickPosition
+ );
+ setTempo(msg==null ? null : msg.getData());
+ }
public void clear() { setTempo(DEFAULT_QPM); }
}
package camidion.chordhelper.midieditor;
+import javax.sound.midi.MetaMessage;
import javax.swing.JComboBox;
import javax.swing.JLabel;
import javax.swing.JPanel;
public void setValue(byte[] data) {
if(data == null) clear(); else setValue(data[0], data[1]);
}
+ public void setValueAt(SequenceTickIndex tickIndex, long tickPosition) {
+ MetaMessage msg = tickIndex.lastMetaMessageAt(
+ SequenceTickIndex.MetaMessageType.TIME_SIGNATURE,
+ tickPosition
+ );
+ setValue(msg==null ? null : msg.getData());
+ }
private boolean editable;
public boolean isEditable() { return editable; }
public void setEditable(boolean editable) {
* MIDIトラックの仕様を表すクラス
*/
public abstract class AbstractTrackSpec {
- public static final int BEAT_RESOLUTION = 2;
- // 最短の音符の長さ(四分音符を何回半分にするか)
public String name = null;
Track track = null;
FirstTrackSpec firstTrackSpec = null;