1 import java.awt.Color;
\r
2 import java.awt.Component;
\r
3 import java.awt.Desktop;
\r
4 import java.awt.Dimension;
\r
5 import java.awt.Graphics;
\r
6 import java.awt.Image;
\r
7 import java.awt.Insets;
\r
8 import java.awt.dnd.DnDConstants;
\r
9 import java.awt.dnd.DropTarget;
\r
10 import java.awt.event.ActionEvent;
\r
11 import java.awt.event.ActionListener;
\r
12 import java.awt.event.InputEvent;
\r
13 import java.awt.event.ItemEvent;
\r
14 import java.awt.event.ItemListener;
\r
15 import java.awt.event.MouseAdapter;
\r
16 import java.awt.event.MouseEvent;
\r
17 import java.awt.event.MouseListener;
\r
18 import java.io.IOException;
\r
19 import java.io.UnsupportedEncodingException;
\r
20 import java.net.URI;
\r
21 import java.net.URISyntaxException;
\r
22 import java.net.URL;
\r
23 import java.security.AccessControlException;
\r
24 import java.util.Arrays;
\r
25 import java.util.Vector;
\r
27 import javax.sound.midi.InvalidMidiDataException;
\r
28 import javax.sound.midi.MetaEventListener;
\r
29 import javax.sound.midi.MetaMessage;
\r
30 import javax.sound.midi.Sequence;
\r
31 import javax.sound.midi.Sequencer;
\r
32 import javax.swing.Box;
\r
33 import javax.swing.BoxLayout;
\r
34 import javax.swing.ButtonGroup;
\r
35 import javax.swing.ImageIcon;
\r
36 import javax.swing.JApplet;
\r
37 import javax.swing.JButton;
\r
38 import javax.swing.JCheckBoxMenuItem;
\r
39 import javax.swing.JComponent;
\r
40 import javax.swing.JEditorPane;
\r
41 import javax.swing.JLabel;
\r
42 import javax.swing.JOptionPane;
\r
43 import javax.swing.JPanel;
\r
44 import javax.swing.JPopupMenu;
\r
45 import javax.swing.JRadioButtonMenuItem;
\r
46 import javax.swing.JSlider;
\r
47 import javax.swing.JSplitPane;
\r
48 import javax.swing.JTextField;
\r
49 import javax.swing.JToggleButton;
\r
50 import javax.swing.SwingUtilities;
\r
51 import javax.swing.border.Border;
\r
52 import javax.swing.event.ChangeEvent;
\r
53 import javax.swing.event.ChangeListener;
\r
54 import javax.swing.event.HyperlinkEvent;
\r
55 import javax.swing.event.HyperlinkListener;
\r
56 import javax.swing.event.PopupMenuEvent;
\r
57 import javax.swing.event.PopupMenuListener;
\r
60 * MIDI Chord Helper - Circle-of-fifth oriented chord pad
\r
64 * Copyright (C) 2004-2013 @きよし - Akiyoshi Kamide
\r
65 * http://www.yk.rim.or.jp/~kamide/music/chordhelper/
\r
67 public class ChordHelperApplet extends JApplet {
\r
68 /////////////////////////////////////////////////////////////////////
\r
70 // JavaScript などからの呼び出しインターフェース
\r
72 /////////////////////////////////////////////////////////////////////
\r
74 * 未保存の修正済み MIDI ファイルがあるかどうか調べます。
\r
75 * @return 未保存の修正済み MIDI ファイルがあれば true
\r
77 public boolean isModified() {
\r
78 return editorDialog.sequenceListTable.getModel().isModified();
\r
81 * 指定された小節数の曲を、乱数で自動作曲してプレイリストへ追加します。
\r
82 * @param measureLength 小節数
\r
83 * @return 追加先のインデックス値(0から始まる)。追加できなかったときは -1
\r
85 public int addRandomSongToPlaylist(int measureLength) {
\r
86 editorDialog.newSequenceDialog.setRandomChordProgression(measureLength);
\r
87 Sequence sequence = editorDialog.newSequenceDialog.getMidiSequence();
\r
88 return editorDialog.sequenceListTable.getModel().addSequenceAndPlay(sequence);
\r
91 * URLで指定されたMIDIファイルをプレイリストへ追加します。
\r
93 * <p>URL の最後の / より後ろの部分がファイル名として取り込まれます。
\r
94 * 指定できる MIDI ファイルには、param タグの midi_file パラメータと同様の制限があります。
\r
96 * @param midiFileUrl 追加するMIDIファイルのURL
\r
97 * @return 追加先のインデックス値(0から始まる)。追加できなかったときは -1
\r
99 public int addToPlaylist(String midiFileUrl) {
\r
101 return editorDialog.sequenceListTable.getModel().addSequenceFromURL(midiFileUrl);
\r
102 } catch( URISyntaxException|IOException|InvalidMidiDataException e ) {
\r
103 editorDialog.showWarning(e.getMessage());
\r
104 } catch( AccessControlException e ) {
\r
105 e.printStackTrace();
\r
106 editorDialog.showError(e.getMessage());
\r
111 * Base64 エンコードされた MIDI ファイルをプレイリストへ追加します。
\r
113 * @param base64EncodedText Base64エンコードされたMIDIファイル
\r
114 * @return 追加先のインデックス値(0から始まる)。追加できなかったときは -1
\r
116 public int addToPlaylistBase64(String base64EncodedText) {
\r
117 return addToPlaylistBase64(base64EncodedText, null);
\r
121 * Base64エンコードされたMIDIファイルをプレイリストへ追加します。
\r
123 * @param base64EncodedText Base64エンコードされたMIDIファイル
\r
124 * @param filename ディレクトリ名を除いたファイル名
\r
125 * @return 追加先のインデックス値(0から始まる)。追加できなかったときは -1
\r
127 public int addToPlaylistBase64(String base64EncodedText, String filename) {
\r
128 Base64Dialog d = editorDialog.base64Dialog;
\r
129 d.setBase64Data(base64EncodedText);
\r
131 return editorDialog.sequenceListTable.getModel().addSequence(d.getMIDIData(), filename);
\r
132 } catch (IOException | InvalidMidiDataException e) {
\r
133 e.printStackTrace();
\r
134 editorDialog.showWarning(e.getMessage());
\r
139 * プレイリスト上で現在選択されているMIDIシーケンスを、
\r
140 * シーケンサへロードして再生します。
\r
142 public void play() {
\r
143 play(editorDialog.sequenceListTable.getModel().sequenceListSelectionModel.getMinSelectionIndex());
\r
146 * 指定されたインデックス値が示すプレイリスト上のMIDIシーケンスを、
\r
147 * シーケンサへロードして再生します。
\r
148 * @param index インデックス値(0から始まる)
\r
150 public void play(int index) {
\r
151 editorDialog.sequenceListTable.getModel().loadToSequencer(index);
\r
152 deviceModelList.sequencerModel.start();
\r
155 * シーケンサが実行中かどうかを返します。
\r
156 * {@link Sequencer#isRunning()} の戻り値をそのまま返します。
\r
158 * @return 実行中のときtrue
\r
160 public boolean isRunning() {
\r
161 return deviceModelList.sequencerModel.getSequencer().isRunning();
\r
164 * シーケンサが再生中かどうかを返します。
\r
165 * @return 再生中のときtrue
\r
167 public boolean isPlaying() { return isRunning(); }
\r
169 * 現在シーケンサにロードされているMIDIデータを
\r
170 * Base64テキストに変換した結果を返します。
\r
171 * @return MIDIデータをBase64テキストに変換した結果
\r
173 public String getMidiDataBase64() {
\r
174 SequenceTrackListTableModel sequenceModel =
\r
175 editorDialog.sequenceListTable.getModel().sequencerModel.getSequenceTableModel();
\r
176 editorDialog.base64Dialog.setMIDIData(sequenceModel.getMIDIdata());
\r
177 return editorDialog.base64Dialog.getBase64Data();
\r
180 * 現在シーケンサにロードされているMIDIファイルのファイル名を返します。
\r
181 * @return MIDIファイル名(設定されていないときは空文字列)
\r
183 public String getMidiFilename() {
\r
184 SequenceTrackListTableModel seq_model = deviceModelList.sequencerModel.getSequenceTableModel();
\r
185 if( seq_model == null ) return null;
\r
186 String fn = seq_model.getFilename();
\r
187 return fn == null ? "" : fn ;
\r
191 * @param octavePosition オクターブ位置(デフォルト:4)
\r
193 public void setOctavePosition(int octavePosition) {
\r
194 keyboardPanel.keyboardCenterPanel.keyboard.octaveRangeModel.setValue(octavePosition);
\r
197 * 操作対象のMIDIチャンネルを変更します。
\r
198 * @param ch チャンネル番号 - 1(チャンネル1のとき0、デフォルトは0)
\r
200 public void setChannel(int ch) {
\r
201 keyboardPanel.keyboardCenterPanel.keyboard.midiChComboboxModel.setSelectedChannel(ch);
\r
204 * 操作対象のMIDIチャンネルを返します。
\r
205 * @return 操作対象のMIDIチャンネル
\r
207 public int getChannel() {
\r
208 return keyboardPanel.keyboardCenterPanel.keyboard.midiChComboboxModel.getSelectedChannel();
\r
211 * 操作対象のMIDIチャンネルに対してプログラム(音色)を設定します。
\r
212 * @param program 音色(0~127:General MIDI に基づく)
\r
214 public void programChange(int program) {
\r
215 keyboardPanel.keyboardCenterPanel.keyboard.getSelectedChannel().programChange(program);
\r
218 * 操作対象のMIDIチャンネルに対してプログラム(音色)を設定します。
\r
219 * 内部的には {@link #programChange(int)} を呼び出しているだけです。
\r
220 * @param program 音色(0~127:General MIDI に基づく)
\r
222 public void setProgram(int program) { programChange(program); }
\r
224 * 自動転回モードを変更します。初期値は true です。
\r
225 * @param isAuto true:自動転回を行う false:自動転回を行わない
\r
227 public void setAutoInversion(boolean isAuto) {
\r
228 inversionOmissionButton.setAutoInversion(isAuto);
\r
234 * <li>-1:省略しない(デフォルト)</li>
\r
235 * <li>0:ルート音を省略</li>
\r
240 public void setOmissionNoteIndex(int index) {
\r
241 inversionOmissionButton.setOmissionNoteIndex(index);
\r
244 * コードダイアグラムの表示・非表示を切り替えます。
\r
245 * @param isVisible 表示するときtrue
\r
247 public void setChordDiagramVisible(boolean isVisible) {
\r
248 keyboardSplitPane.resetToPreferredSizes();
\r
250 keyboardSplitPane.setDividerLocation((double)1.0);
\r
253 * コードダイヤグラムをギターモードに変更します。
\r
254 * 初期状態ではウクレレモードになっています。
\r
256 public void setChordDiagramForGuitar() {
\r
257 chordDiagram.setTargetInstrument(ChordDiagram.TargetInstrument.Guitar);
\r
260 * ダークモード(暗い表示)と明るい表示とを切り替えます。
\r
261 * @param isDark ダークモードのときtrue、明るい表示のときfalse(デフォルト)
\r
263 public void setDarkMode(boolean isDark) {
\r
264 darkModeToggleButton.setSelected(isDark);
\r
269 public static class VersionInfo {
\r
270 public static final String NAME = "MIDI Chord Helper";
\r
271 public static final String VERSION = "Ver.20131206.1";
\r
272 public static final String COPYRIGHT = "Copyright (C) 2004-2013";
\r
273 public static final String AUTHER = "@きよし - Akiyoshi Kamide";
\r
274 public static final String URL = "http://www.yk.rim.or.jp/~kamide/music/chordhelper/";
\r
275 public static String getInfo() {
\r
276 return NAME + " " + VERSION + " " + COPYRIGHT + " " + AUTHER + " " + URL;
\r
280 public String getAppletInfo() {
\r
281 return VersionInfo.getInfo();
\r
283 private class AboutMessagePane extends JEditorPane implements ActionListener {
\r
285 public AboutMessagePane() { this(true); }
\r
286 public AboutMessagePane(boolean link_enabled) {
\r
287 super( "text/html", "" );
\r
288 String link_string, tooltip = null;
\r
289 if( link_enabled && Desktop.isDesktopSupported() ) {
\r
290 tooltip = "Click this URL to open with your web browser - URLをクリックしてWebブラウザで開く";
\r
292 "<a href=\"" + VersionInfo.URL + "\" title=\"" +
\r
293 tooltip + "\">" + VersionInfo.URL + "</a>" ;
\r
296 link_enabled = false; link_string = VersionInfo.URL;
\r
299 "<html><center><font size=\"+1\">" + VersionInfo.NAME + "</font> " +
\r
300 VersionInfo.VERSION + "<br/><br/>" +
\r
301 VersionInfo.COPYRIGHT + " " + VersionInfo.AUTHER + "<br/>" +
\r
302 link_string + "</center></html>"
\r
304 setToolTipText(tooltip);
\r
306 putClientProperty(JEditorPane.HONOR_DISPLAY_PROPERTIES, Boolean.TRUE);
\r
307 setEditable(false);
\r
309 // メッセージ内の <a href=""> ~ </a> によるリンクを
\r
310 // 実際に機能させる(ブラウザで表示されるようにする)ための設定
\r
312 if( ! link_enabled ) return;
\r
314 uri = new URI(VersionInfo.URL);
\r
315 }catch( URISyntaxException use ) {
\r
316 use.printStackTrace();
\r
319 addHyperlinkListener(new HyperlinkListener() {
\r
320 public void hyperlinkUpdate(HyperlinkEvent e) {
\r
321 if(e.getEventType()==HyperlinkEvent.EventType.ACTIVATED) {
\r
323 Desktop.getDesktop().browse(uri);
\r
324 }catch(IOException ioe) {
\r
325 ioe.printStackTrace();
\r
332 public void actionPerformed(ActionEvent e) {
\r
333 JOptionPane.showMessageDialog(
\r
334 null, this, "Version info",
\r
335 JOptionPane.INFORMATION_MESSAGE, imageIcon
\r
340 public boolean isConfirmedToExit() {
\r
341 return ! isModified() || JOptionPane.showConfirmDialog(
\r
343 "MIDI file not saved, exit anyway ?\n保存されていないMIDIファイルがありますが、終了してよろしいですか?",
\r
345 JOptionPane.YES_NO_OPTION,
\r
346 JOptionPane.WARNING_MESSAGE
\r
347 ) == JOptionPane.YES_OPTION ;
\r
349 // アプリケーションのアイコンイメージ
\r
350 public ImageIcon imageIcon;
\r
351 // ボタンの余白を詰めたいときは setMargin() の引数にこれを指定する
\r
352 public static final Insets ZERO_INSETS = new Insets(0,0,0,0);
\r
354 private JPanel keyboardSequencerPanel;
\r
355 private JPanel chordGuide;
\r
356 private Color rootPaneDefaultBgcolor;
\r
357 private Color lyricDisplayDefaultBgcolor;
\r
358 private Border lyricDisplayDefaultBorder;
\r
359 private JSplitPane mainSplitPane;
\r
360 private JSplitPane keyboardSplitPane;
\r
361 private ChordButtonLabel enterButtonLabel;
\r
362 private ChordTextField lyricDisplay;
\r
363 private MidiKeyboardPanel keyboardPanel;
\r
364 ChordMatrix chordMatrix;
\r
365 private InversionAndOmissionLabel inversionOmissionButton;
\r
366 private JToggleButton darkModeToggleButton;
\r
367 MidiDeviceModelList deviceModelList;
\r
368 MidiDeviceDialog midiConnectionDialog;
\r
369 MidiEditor editorDialog;
\r
370 ChordDiagram chordDiagram;
\r
371 TempoSelecter tempoSelecter;
\r
372 TimeSignatureSelecter timesigSelecter;
\r
373 KeySignatureLabel keysigLabel;
\r
374 JLabel songTitleLabel = new JLabel();
\r
377 AnoGakkiLayeredPane anoGakkiLayeredPane;
\r
378 JToggleButton anoGakkiToggleButton;
\r
380 public void init() {
\r
381 String imageIconPath = "images/midichordhelper.png";
\r
382 URL imageIconUrl = getClass().getResource(imageIconPath);
\r
383 if( imageIconUrl == null ) {
\r
384 // System.out.println("icon "+imageIconPath+" not found");
\r
388 imageIcon = new ImageIcon(imageIconUrl);
\r
390 Image iconImage = (imageIcon == null) ? null : imageIcon.getImage();
\r
391 rootPaneDefaultBgcolor = getContentPane().getBackground();
\r
392 chordMatrix = new ChordMatrix() {{
\r
393 addChordMatrixListener(new ChordMatrixListener(){
\r
394 public void keySignatureChanged() {
\r
395 Music.Key capoKey = getKeySignatureCapo();
\r
396 keyboardPanel.keySelecter.setKey(capoKey);
\r
397 keyboardPanel.keyboardCenterPanel.keyboard.setKeySignature(capoKey);
\r
399 public void chordChanged() { chordOn(); }
\r
402 chordMatrix.capoSelecter.checkbox.addItemListener(
\r
403 new ItemListener() {
\r
404 public void itemStateChanged(ItemEvent e) {
\r
406 keyboardPanel.keyboardCenterPanel.keyboard.chordDisplay.setNote(-1);
\r
407 chordDiagram.clear();
\r
411 chordMatrix.capoSelecter.valueSelecter.addActionListener(
\r
412 new ActionListener() {
\r
413 public void actionPerformed(ActionEvent e) {
\r
415 keyboardPanel.keyboardCenterPanel.keyboard.chordDisplay.setNote(-1);
\r
416 chordDiagram.clear();
\r
420 keyboardPanel = new MidiKeyboardPanel(chordMatrix) {{
\r
421 keyboardCenterPanel.keyboard.addPianoKeyboardListener(
\r
422 new PianoKeyboardAdapter() {
\r
423 public void pianoKeyPressed(int n, InputEvent e) {
\r
424 chordDiagram.clear();
\r
428 keySelecter.keysigCombobox.addActionListener(
\r
429 new ActionListener() {
\r
430 public void actionPerformed(ActionEvent e) {
\r
431 Music.Key key = keySelecter.getKey();
\r
432 key.transpose( - chordMatrix.capoSelecter.getCapo() );
\r
433 chordMatrix.setKeySignature(key);
\r
437 keyboardCenterPanel.keyboard.setPreferredSize(new Dimension(571, 80));
\r
439 deviceModelList = new MidiDeviceModelList(
\r
440 new Vector<VirtualMidiDevice>() {
\r
442 add(keyboardPanel.keyboardCenterPanel.keyboard.midiDevice);
\r
446 editorDialog = new MidiEditor(deviceModelList.sequencerModel);
\r
447 editorDialog.setIconImage(iconImage);
\r
448 deviceModelList.setMidiEditor(editorDialog);
\r
449 new DropTarget(this, DnDConstants.ACTION_COPY_OR_MOVE, editorDialog, true);
\r
450 keyboardPanel.eventDialog = editorDialog.eventDialog;
\r
451 midiConnectionDialog = new MidiDeviceDialog(deviceModelList);
\r
452 midiConnectionDialog.setIconImage(iconImage);
\r
453 lyricDisplay = new ChordTextField() {
\r
455 deviceModelList.sequencerModel.getSequencer().addMetaEventListener(this);
\r
457 new ActionListener() {
\r
458 public void actionPerformed(ActionEvent event) {
\r
459 chordMatrix.setSelectedChord(
\r
460 event.getActionCommand().trim().split("[ \t\r\n]")[0]
\r
467 lyricDisplayDefaultBorder = lyricDisplay.getBorder();
\r
468 lyricDisplayDefaultBgcolor = lyricDisplay.getBackground();
\r
472 chordDiagram = new ChordDiagram(this);
\r
474 // MetaEvent listeners
\r
476 tempoSelecter = new TempoSelecter() {{
\r
477 setEditable(false);
\r
478 deviceModelList.sequencerModel.getSequencer().addMetaEventListener(this);
\r
480 timesigSelecter = new TimeSignatureSelecter() {{
\r
481 setEditable(false);
\r
482 deviceModelList.sequencerModel.getSequencer().addMetaEventListener(this);
\r
484 keysigLabel = new KeySignatureLabel() {{
\r
485 addMouseListener(new MouseAdapter() {
\r
486 public void mousePressed(MouseEvent e) {
\r
487 chordMatrix.setKeySignature(getKey());
\r
491 deviceModelList.sequencerModel.getSequencer().addMetaEventListener(
\r
492 new MetaEventListener() {
\r
493 class SetKeySignatureRunnable implements Runnable {
\r
495 public SetKeySignatureRunnable(Music.Key key) {
\r
499 public void run() { setKeySignature(key); }
\r
502 public void meta(MetaMessage msg) {
\r
503 switch(msg.getType()) {
\r
504 case 0x59: // Key signature (2 bytes) : 調号
\r
505 Music.Key key = new Music.Key(msg.getData());
\r
506 if( ! SwingUtilities.isEventDispatchThread() ) {
\r
507 SwingUtilities.invokeLater(
\r
508 new SetKeySignatureRunnable(key)
\r
511 setKeySignature(key);
\r
515 private void setKeySignature(Music.Key key) {
\r
516 keysigLabel.setKeySignature(key);
\r
517 chordMatrix.setKeySignature(key);
\r
521 //シーケンサーの時間スライダーの値が変わったときのリスナーを登録
\r
522 deviceModelList.sequencerModel.addChangeListener(new ChangeListener() {
\r
524 public void stateChanged(ChangeEvent e) {
\r
525 SequenceTrackListTableModel sequenceTableModel = deviceModelList.sequencerModel.getSequenceTableModel();
\r
526 int loadedSequenceIndex = editorDialog.sequenceListTable.getModel().indexOfSequenceOnSequencer();
\r
527 songTitleLabel.setText(
\r
529 loadedSequenceIndex < 0 ? "[No MIDI file loaded]" :
\r
530 "MIDI file " + loadedSequenceIndex + ": " + (
\r
531 sequenceTableModel == null ||
\r
532 sequenceTableModel.toString() == null ||
\r
533 sequenceTableModel.toString().isEmpty() ?
\r
535 "<font color=maroon>"+sequenceTableModel+"</font>"
\r
539 Sequencer sequencer = deviceModelList.sequencerModel.getSequencer();
\r
540 chordMatrix.setPlaying(sequencer.isRunning());
\r
541 if( sequenceTableModel != null ) {
\r
542 SequenceTickIndex tickIndex = sequenceTableModel.getSequenceTickIndex();
\r
543 long tickPos = sequencer.getTickPosition();
\r
544 tickIndex.tickToMeasure(tickPos);
\r
545 chordMatrix.setBeat(tickIndex);
\r
547 deviceModelList.sequencerModel.getValueIsAdjusting() ||
\r
548 ! (sequencer.isRunning() || sequencer.isRecording())
\r
551 msg = tickIndex.lastMetaMessageAt(SequenceTickIndex.TIME_SIGNATURE, tickPos);
\r
552 timesigSelecter.setValue(msg==null ? null : msg.getData());
\r
553 msg = tickIndex.lastMetaMessageAt(SequenceTickIndex.TEMPO, tickPos);
\r
554 tempoSelecter.setTempo(msg==null ? null : msg.getData());
\r
555 msg = tickIndex.lastMetaMessageAt(SequenceTickIndex.KEY_SIGNATURE, tickPos);
\r
557 keysigLabel.clear();
\r
559 Music.Key key = new Music.Key(msg.getData());
\r
560 keysigLabel.setKeySignature(key);
\r
561 chordMatrix.setKeySignature(key);
\r
567 deviceModelList.sequencerModel.fireStateChanged();
\r
568 chordGuide = new JPanel() {
\r
570 setLayout(new BoxLayout(this, BoxLayout.X_AXIS));
\r
571 add( Box.createHorizontalStrut(2) );
\r
572 add( chordMatrix.chordGuide );
\r
573 add( Box.createHorizontalStrut(2) );
\r
574 add( lyricDisplay );
\r
575 add( Box.createHorizontalStrut(2) );
\r
576 add( enterButtonLabel = new ChordButtonLabel("Enter",chordMatrix) {{
\r
577 addMouseListener(new MouseAdapter() {
\r
578 public void mousePressed(MouseEvent e) {
\r
579 if( (e.getModifiersEx() & InputEvent.BUTTON3_DOWN_MASK) != 0 ) // RightClicked
\r
580 chordMatrix.setSelectedChord( (Music.Chord)null );
\r
582 chordMatrix.setSelectedChord( lyricDisplay.getText() );
\r
586 add( Box.createHorizontalStrut(5) );
\r
587 add( chordMatrix.chordDisplay );
\r
588 add( Box.createHorizontalStrut(5) );
\r
589 add( darkModeToggleButton = new JToggleButton(new ButtonIcon(ButtonIcon.DARK_MODE_ICON)) {{
\r
590 setMargin(ZERO_INSETS);
\r
591 addItemListener(new ItemListener() {
\r
592 public void itemStateChanged(ItemEvent e) {
\r
593 innerSetDarkMode(darkModeToggleButton.isSelected());
\r
596 setToolTipText("Light / Dark - 明かりを点灯/消灯");
\r
599 add( Box.createHorizontalStrut(5) );
\r
600 add( anoGakkiToggleButton = new JToggleButton(
\r
601 new ButtonIcon(ButtonIcon.ANO_GAKKI_ICON)
\r
604 setMargin(ZERO_INSETS);
\r
606 setToolTipText("あの楽器");
\r
608 new ItemListener() {
\r
609 public void itemStateChanged(ItemEvent e) {
\r
610 keyboardPanel.keyboardCenterPanel.keyboard.anoGakkiLayeredPane
\r
611 = anoGakkiToggleButton.isSelected() ? anoGakkiLayeredPane : null ;
\r
616 add( Box.createHorizontalStrut(5) );
\r
617 add( inversionOmissionButton = new InversionAndOmissionLabel() );
\r
618 add( Box.createHorizontalStrut(5) );
\r
619 add( chordMatrix.capoSelecter );
\r
620 add( Box.createHorizontalStrut(2) );
\r
623 keyboardSequencerPanel = new JPanel() {{
\r
624 setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
\r
626 add(Box.createVerticalStrut(5));
\r
627 add(keyboardSplitPane = new JSplitPane(
\r
628 JSplitPane.HORIZONTAL_SPLIT, keyboardPanel, chordDiagram
\r
630 setOneTouchExpandable(true);
\r
631 setResizeWeight(1.0);
\r
632 setAlignmentX((float)0.5);
\r
634 add(Box.createVerticalStrut(5));
\r
635 add(new JPanel() {{
\r
636 setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
\r
637 add(new JPanel() {{
\r
638 setLayout(new BoxLayout(this, BoxLayout.X_AXIS));
\r
639 add( Box.createHorizontalStrut(12) );
\r
640 add( keysigLabel );
\r
641 add( Box.createHorizontalStrut(12) );
\r
642 add( timesigSelecter );
\r
643 add( Box.createHorizontalStrut(12) );
\r
644 add( tempoSelecter );
\r
645 add( Box.createHorizontalStrut(12) );
\r
646 add( new MeasureIndicator(deviceModelList.sequencerModel) );
\r
647 add( Box.createHorizontalStrut(12) );
\r
648 add( songTitleLabel );
\r
649 add( Box.createHorizontalStrut(12) );
\r
650 add( new JButton(editorDialog.openAction) {{ setMargin(ZERO_INSETS); }});
\r
652 add(new JPanel() {{
\r
653 setLayout(new BoxLayout(this, BoxLayout.X_AXIS));
\r
654 add( Box.createHorizontalStrut(10) );
\r
655 add( new JSlider(deviceModelList.sequencerModel) );
\r
656 add( new TimeIndicator(deviceModelList.sequencerModel) );
\r
657 add( Box.createHorizontalStrut(5) );
\r
658 add( new JButton(editorDialog.sequenceListTable.getModel().moveToTopAction) {{
\r
659 setMargin(ZERO_INSETS);
\r
661 add(new JButton(deviceModelList.sequencerModel.moveBackwardAction) {{
\r
662 setMargin(ZERO_INSETS);
\r
664 add(new JToggleButton(deviceModelList.sequencerModel.startStopAction));
\r
665 add(new JButton(deviceModelList.sequencerModel.moveForwardAction) {{
\r
666 setMargin(ZERO_INSETS);
\r
668 add(new JButton(editorDialog.sequenceListTable.getModel().moveToBottomAction) {{
\r
669 setMargin(ZERO_INSETS);
\r
671 add(new JToggleButton(editorDialog.sequenceListTable.getModel().toggleRepeatAction) {{
\r
672 setMargin(ZERO_INSETS);
\r
674 add( Box.createHorizontalStrut(10) );
\r
676 add(new JPanel() {{
\r
678 "MIDI device connection",
\r
679 new ButtonIcon( ButtonIcon.MIDI_CONNECTOR_ICON )
\r
681 addActionListener(midiConnectionDialog);
\r
683 add(new JButton("Version info") {{
\r
684 setToolTipText(VersionInfo.NAME + " " + VersionInfo.VERSION);
\r
685 addActionListener(new AboutMessagePane());
\r
690 mainSplitPane = new JSplitPane(
\r
691 JSplitPane.VERTICAL_SPLIT, chordMatrix, keyboardSequencerPanel
\r
693 setResizeWeight(0.5);
\r
694 setAlignmentX((float)0.5);
\r
697 anoGakkiLayeredPane = new AnoGakkiLayeredPane() {{ add(mainSplitPane); }};
\r
698 setContentPane(anoGakkiLayeredPane);
\r
699 setPreferredSize(new Dimension(750,470));
\r
701 /////////////////////////////////////////
\r
705 public void start() {
\r
707 // コードボタンで設定されている現在の調を
\r
709 chordMatrix.fireKeySignatureChanged();
\r
711 // アプレットのパラメータにMIDIファイルのURLが指定されていたら
\r
713 String midi_url = getParameter("midi_file");
\r
715 if( midi_url != null ) {
\r
716 addToPlaylist(midi_url);
\r
721 public void stop() {
\r
722 deviceModelList.sequencerModel.stop(); // MIDI再生を強制終了
\r
725 private void innerSetDarkMode(boolean is_dark) {
\r
726 Color col = is_dark ? Color.black : null;
\r
727 // Color fgcol = is_dark ? Color.pink : null;
\r
728 getContentPane().setBackground(
\r
729 is_dark ? Color.black : rootPaneDefaultBgcolor
\r
731 mainSplitPane.setBackground( col );
\r
732 keyboardSplitPane.setBackground( col );
\r
733 enterButtonLabel.setDarkMode( is_dark );
\r
734 chordGuide.setBackground( col );
\r
735 lyricDisplay.setBorder( is_dark ? null : lyricDisplayDefaultBorder );
\r
736 lyricDisplay.setBackground( is_dark ?
\r
737 chordMatrix.darkModeColorset.backgrounds[2] : lyricDisplayDefaultBgcolor
\r
739 lyricDisplay.setForeground( is_dark ? Color.white : null );
\r
740 inversionOmissionButton.setBackground( col );
\r
741 anoGakkiToggleButton.setBackground( col );
\r
742 keyboardSequencerPanel.setBackground( col );
\r
743 chordDiagram.setBackground( col );
\r
744 chordDiagram.titleLabel.setDarkMode( is_dark );
\r
745 chordMatrix.setDarkMode( is_dark );
\r
746 keyboardPanel.setDarkMode( is_dark );
\r
749 private int[] chordOnNotes = null;
\r
752 * <p>この関数を直接呼ぶとアルペジオが効かないので、
\r
753 * chord_matrix.setSelectedChord() を使うことを推奨
\r
756 public void chordOn() {
\r
757 Music.Chord playChord = chordMatrix.getSelectedChord();
\r
759 chordOnNotes != null &&
\r
760 chordMatrix.getNoteIndex() < 0 &&
\r
761 (! chordMatrix.isDragged() || playChord == null)
\r
763 // コードが鳴っている状態で、新たなコードを鳴らそうとしたり、
\r
764 // もう鳴らさないという信号が来た場合は、今鳴っている音を止める。
\r
766 for( int n : chordOnNotes )
\r
767 keyboardPanel.keyboardCenterPanel.keyboard.noteOff(n);
\r
768 chordOnNotes = null;
\r
770 if( playChord == null ) {
\r
771 // もう鳴らさないので、歌詞表示に通知して終了
\r
772 if( lyricDisplay != null )
\r
773 lyricDisplay.currentChord = null;
\r
777 if( keyboardPanel.keyboardCenterPanel.keyboard.anoGakkiLayeredPane != null ) {
\r
778 JComponent btn = chordMatrix.getSelectedButton();
\r
780 anoGakkiLayeredPane.start(chordMatrix,btn);
\r
782 // コードボタンからのコードを、カポつき演奏キーからオリジナルキーへ変換
\r
783 Music.Key originalKey = chordMatrix.getKeySignatureCapo();
\r
784 Music.Chord originalChord = playChord.clone().transpose(
\r
785 chordMatrix.capoSelecter.getCapo(),
\r
786 chordMatrix.getKeySignature()
\r
788 // 変換後のコードをキーボード画面に設定
\r
789 keyboardPanel.keyboardCenterPanel.keyboard.setChord(originalChord);
\r
791 // 音域を決める。これにより鳴らす音が確定する。
\r
792 Music.Range chordRange = new Music.Range(
\r
793 keyboardPanel.keyboardCenterPanel.keyboard.getChromaticOffset() + 10 +
\r
794 ( keyboardPanel.keyboardCenterPanel.keyboard.getOctaves() / 4 ) * 12,
\r
795 inversionOmissionButton.isAutoInversionMode() ?
\r
796 keyboardPanel.keyboardCenterPanel.keyboard.getChromaticOffset() + 21 :
\r
797 keyboardPanel.keyboardCenterPanel.keyboard.getChromaticOffset() + 33,
\r
799 inversionOmissionButton.isAutoInversionMode()
\r
801 int[] notes = originalChord.toNoteArray(chordRange, originalKey);
\r
803 // 前回鳴らしたコード構成音を覚えておく
\r
804 int[] prevChordOnNotes = null;
\r
805 if( chordMatrix.isDragged() || chordMatrix.getNoteIndex() >= 0 )
\r
806 prevChordOnNotes = Arrays.copyOf(chordOnNotes, chordOnNotes.length);
\r
809 chordOnNotes = new int[notes.length];
\r
811 for( int n : notes ) {
\r
812 if( inversionOmissionButton.getOmissionNoteIndex() == i ) {
\r
815 chordOnNotes[i++] = n;
\r
818 boolean isNoteOn = false;
\r
819 if( prevChordOnNotes != null ) {
\r
820 for( int prevN : prevChordOnNotes ) {
\r
827 // すでに鳴っているのに単音を鳴らそうとする場合、
\r
828 // 鳴らそうとしている音を一旦止める。
\r
829 if( isNoteOn && chordMatrix.getNoteIndex() >= 0 &&
\r
830 notes[chordMatrix.getNoteIndex()] - n == 0
\r
832 keyboardPanel.keyboardCenterPanel.keyboard.noteOff(n);
\r
835 // その音が鳴っていなかったら鳴らす。
\r
837 keyboardPanel.keyboardCenterPanel.keyboard.noteOn(n);
\r
841 keyboardPanel.keyboardCenterPanel.keyboard.setChord(originalChord);
\r
842 chordMatrix.chordDisplay.setChord(playChord);
\r
844 // コードダイアグラム用にもコードを表示
\r
845 Music.Chord diagramChord;
\r
846 int chordDiagramCapo = chordDiagram.capoSelecterView.getCapo();
\r
847 if( chordDiagramCapo == chordMatrix.capoSelecter.getCapo() )
\r
848 diagramChord = playChord.clone();
\r
850 diagramChord = originalChord.clone().transpose(
\r
851 - chordDiagramCapo, originalKey
\r
853 chordDiagram.setChord(diagramChord);
\r
854 if( chordDiagram.recordTextButton.isSelected() )
\r
855 lyricDisplay.appendChord(diagramChord);
\r
859 /***************************************************************************
\r
863 ***************************************************************************/
\r
865 class ChordDisplay extends JLabel {
\r
866 Music.Chord chord = null;
\r
867 PianoKeyboard keyboard = null;
\r
868 ChordMatrix chordMatrix = null;
\r
869 String defaultString = null;
\r
870 int noteNumber = -1;
\r
871 private boolean isDark = false;
\r
872 private boolean isMouseEntered = false;
\r
874 public ChordDisplay(String defaultString, ChordMatrix chordMatrix, PianoKeyboard keyboard) {
\r
875 super(defaultString, JLabel.CENTER);
\r
876 this.defaultString = defaultString;
\r
877 this.keyboard = keyboard;
\r
878 this.chordMatrix = chordMatrix;
\r
879 if( chordMatrix != null ) {
\r
880 addMouseListener(new MouseAdapter() {
\r
881 public void mousePressed(MouseEvent e) {
\r
882 if( chord != null ) { // コードが表示されている場合
\r
883 if( (e.getModifiersEx() & InputEvent.BUTTON3_DOWN_MASK) != 0 ) {
\r
885 ChordDisplay.this.chordMatrix.setSelectedChord((Music.Chord)null);
\r
889 // キーボードが指定されている場合、オリジナルキー(カポ反映済)のコードを使う。
\r
890 if( ChordDisplay.this.keyboard == null )
\r
891 ChordDisplay.this.chordMatrix.setSelectedChord(chord);
\r
893 ChordDisplay.this.chordMatrix.setSelectedChordCapo(chord);
\r
896 else if( noteNumber >= 0 ) { // 音階が表示されている場合
\r
897 ChordDisplay.this.keyboard.noteOn(noteNumber);
\r
900 public void mouseReleased(MouseEvent e) {
\r
901 if( noteNumber >= 0 )
\r
902 ChordDisplay.this.keyboard.noteOff(noteNumber);
\r
904 public void mouseEntered(MouseEvent e) {
\r
905 mouseEntered(true);
\r
907 public void mouseExited(MouseEvent e) {
\r
908 mouseEntered(false);
\r
910 private void mouseEntered(boolean isMouseEntered) {
\r
911 ChordDisplay.this.isMouseEntered = isMouseEntered;
\r
912 if( noteNumber >= 0 || chord != null )
\r
916 addMouseWheelListener(this.chordMatrix);
\r
919 public void paint(Graphics g) {
\r
921 Dimension d = getSize();
\r
922 if( isMouseEntered && (noteNumber >= 0 || chord != null) ) {
\r
923 g.setColor(Color.gray);
\r
924 g.drawRect( 0, 0, d.width-1, d.height-1 );
\r
927 private void setChordText() {
\r
928 setText( chord.toHtmlString(isDark ? "#FFCC33" : "maroon") );
\r
930 void setNote(int note_no) { setNote( note_no, false ); }
\r
931 void setNote(int note_no, boolean is_rhythm_part) {
\r
932 setToolTipText(null);
\r
934 this.noteNumber = note_no;
\r
935 if( note_no < 0 ) {
\r
939 setText(defaultString);
\r
942 if( is_rhythm_part ) {
\r
944 "MIDI note No." + note_no + " : "
\r
945 + MIDISpec.getPercussionName(note_no)
\r
950 "Note: " + Music.NoteSymbol.noteNoToSymbol(note_no)
\r
951 + " - MIDI note No." + note_no + " : "
\r
952 + Math.round(Music.noteNoToFrequency(note_no)) + "Hz" );
\r
955 void setChord(Music.Chord chord) {
\r
956 this.chord = chord;
\r
957 this.noteNumber = -1;
\r
958 if( chord == null ) {
\r
959 setText( defaultString );
\r
960 setToolTipText( null );
\r
964 setToolTipText( "Chord: " + chord.toName() );
\r
967 void setDarkMode(boolean is_dark) {
\r
968 this.isDark = is_dark;
\r
969 if( chord != null ) setChordText();
\r
976 class InversionAndOmissionLabel extends JLabel
\r
977 implements MouseListener, PopupMenuListener
\r
979 JPopupMenu popup_menu;
\r
980 ButtonGroup omission_group = new ButtonGroup();
\r
981 ButtonIcon icon = new ButtonIcon(ButtonIcon.INVERSION_ICON);
\r
982 JRadioButtonMenuItem radioButtonitems[] = new JRadioButtonMenuItem[4];
\r
983 JCheckBoxMenuItem cb_inversion;
\r
985 public InversionAndOmissionLabel() {
\r
987 popup_menu = new JPopupMenu();
\r
989 cb_inversion = new JCheckBoxMenuItem("Auto Inversion",true)
\r
991 popup_menu.addSeparator();
\r
992 omission_group.add(
\r
993 radioButtonitems[0] = new JRadioButtonMenuItem("All notes",true)
\r
995 popup_menu.add(radioButtonitems[0]);
\r
996 omission_group.add(
\r
997 radioButtonitems[1] = new JRadioButtonMenuItem("Omit 5th")
\r
999 popup_menu.add(radioButtonitems[1]);
\r
1000 omission_group.add(
\r
1001 radioButtonitems[2] = new JRadioButtonMenuItem("Omit 3rd (Power Chord)")
\r
1003 popup_menu.add(radioButtonitems[2]);
\r
1004 omission_group.add(
\r
1005 radioButtonitems[3] = new JRadioButtonMenuItem("Omit root")
\r
1007 popup_menu.add(radioButtonitems[3]);
\r
1008 addMouseListener(this);
\r
1009 popup_menu.addPopupMenuListener(this);
\r
1010 setToolTipText("Automatic inversion and Note omission - 自動転回と省略音の設定");
\r
1012 public void mousePressed(MouseEvent e) {
\r
1013 Component c = e.getComponent();
\r
1014 if( c == this ) popup_menu.show( c, 0, getHeight() );
\r
1016 public void mouseReleased(MouseEvent e) { }
\r
1017 public void mouseEntered(MouseEvent e) { }
\r
1018 public void mouseExited(MouseEvent e) { }
\r
1019 public void mouseClicked(MouseEvent e) { }
\r
1020 public void popupMenuCanceled(PopupMenuEvent e) { }
\r
1021 public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
\r
1022 repaint(); // To repaint icon image
\r
1024 public void popupMenuWillBecomeVisible(PopupMenuEvent e) { }
\r
1025 public boolean isAutoInversionMode() {
\r
1026 return cb_inversion.isSelected();
\r
1028 public void setAutoInversion(boolean is_auto) {
\r
1029 cb_inversion.setSelected(is_auto);
\r
1031 public int getOmissionNoteIndex() {
\r
1032 if( radioButtonitems[3].isSelected() ) { // Root
\r
1035 else if( radioButtonitems[2].isSelected() ) { // 3rd
\r
1038 else if( radioButtonitems[1].isSelected() ) { // 5th
\r
1041 else { // No omission
\r
1045 public void setOmissionNoteIndex(int index) {
\r
1047 case 0: radioButtonitems[3].setSelected(true); break;
\r
1048 case 1: radioButtonitems[2].setSelected(true); break;
\r
1049 case 2: radioButtonitems[1].setSelected(true); break;
\r
1050 default: radioButtonitems[0].setSelected(true); break;
\r
1055 class ChordTextField extends JTextField implements MetaEventListener {
\r
1056 Music.Chord currentChord = null;
\r
1057 private long lyricArrivedTime = System.nanoTime();
\r
1058 public ChordTextField() {
\r
1061 // JTextField は、サイズ設定をしないとリサイズ時に縦に伸び過ぎてしまう。
\r
1062 // 1行しか入力できないので、縦に伸びすぎるのはスペースがもったいない。
\r
1063 // そこで、このような現象を防止するために、最大サイズを明示的に
\r
1066 // To reduce resized height, set maximum size to screen size.
\r
1069 java.awt.Toolkit.getDefaultToolkit().getScreenSize()
\r
1073 public void meta(MetaMessage msg) {
\r
1074 switch(msg.getType()) {
\r
1075 case 0x01: // Text(任意のテキスト:コメントなど)
\r
1076 case 0x02: // Copyright(著作権表示)
\r
1077 case 0x05: // Lyrics(歌詞)
\r
1078 case 0x06: // Marker
\r
1079 case 0x03: // Sequence Name / Track Name(曲名またはトラック名)
\r
1080 byte[] data = msg.getData();
\r
1081 if( ! SwingUtilities.isEventDispatchThread() ) {
\r
1082 // MIDIシーケンサの EDT から呼ばれた場合、
\r
1083 // 表示処理を Swing の EDT に振り直す。
\r
1084 SwingUtilities.invokeLater(new AddLyricJob(data));
\r
1096 private class AddLyricJob implements Runnable {
\r
1097 private byte[] data;
\r
1098 public AddLyricJob(byte[] data) { this.data = data; }
\r
1100 public void run() { addLyric(data); }
\r
1103 * 歌詞を追加し、カーソルを末尾に移動します。
\r
1104 * @param data 歌詞データ
\r
1106 public void addLyric(byte[] data) {
\r
1107 long startTime = System.nanoTime();
\r
1109 String additionalLyric;
\r
1111 additionalLyric = (new String(data,"JISAutoDetect")).trim();
\r
1112 } catch( UnsupportedEncodingException e ) {
\r
1113 additionalLyric = (new String(data)).trim();
\r
1115 String lyric = getText();
\r
1116 if( startTime - lyricArrivedTime > 1000000000L /* 1sec */
\r
1118 additionalLyric.length() > 8 || additionalLyric.isEmpty()
\r
1119 || lyric == null || lyric.isEmpty()
\r
1122 // 長い歌詞や空白が来たり、追加先に歌詞がなかった場合は上書きする。
\r
1123 // ただし、前回から充分に時間が経っていない場合は上書きしない。
\r
1124 lyric = additionalLyric;
\r
1127 // 短い歌詞だった場合は、既存の歌詞に追加する
\r
1128 lyric += " " + additionalLyric;
\r
1131 setCaretPosition(getText().length());
\r
1132 lyricArrivedTime = startTime;
\r
1136 * @param chord コード
\r
1138 public void appendChord(Music.Chord chord) {
\r
1139 if( currentChord == null && chord == null )
\r
1141 if( currentChord != null && chord != null && chord.equals(currentChord) )
\r
1143 String delimiter = ""; // was "\n"
\r
1144 setText( getText() + (chord == null ? delimiter : chord + " ") );
\r
1145 currentChord = ( chord == null ? null : chord.clone() );
\r