1 package camidion.chordhelper.pianokeyboard;
\r
3 import java.awt.BorderLayout;
\r
4 import java.awt.Color;
\r
5 import java.awt.Dimension;
\r
6 import java.awt.Graphics;
\r
7 import java.awt.Graphics2D;
\r
8 import java.awt.Point;
\r
9 import java.awt.Rectangle;
\r
10 import java.awt.event.ComponentAdapter;
\r
11 import java.awt.event.ComponentEvent;
\r
12 import java.awt.event.FocusEvent;
\r
13 import java.awt.event.FocusListener;
\r
14 import java.awt.event.InputEvent;
\r
15 import java.awt.event.KeyEvent;
\r
16 import java.awt.event.KeyListener;
\r
17 import java.awt.event.MouseEvent;
\r
18 import java.awt.event.MouseListener;
\r
19 import java.awt.event.MouseMotionListener;
\r
20 import java.util.LinkedList;
\r
21 import java.util.Vector;
\r
23 import javax.sound.midi.MidiChannel;
\r
24 import javax.swing.BoundedRangeModel;
\r
25 import javax.swing.DefaultBoundedRangeModel;
\r
26 import javax.swing.JComponent;
\r
27 import javax.swing.event.ChangeEvent;
\r
28 import javax.swing.event.ChangeListener;
\r
29 import javax.swing.event.ListDataEvent;
\r
30 import javax.swing.event.ListDataListener;
\r
32 import camidion.chordhelper.ChordDisplayLabel;
\r
33 import camidion.chordhelper.anogakki.AnoGakkiPane;
\r
34 import camidion.chordhelper.chordmatrix.ChordMatrix;
\r
35 import camidion.chordhelper.mididevice.AbstractMidiChannelStatus;
\r
36 import camidion.chordhelper.mididevice.AbstractMidiStatus;
\r
37 import camidion.chordhelper.mididevice.AbstractVirtualMidiDevice;
\r
38 import camidion.chordhelper.mididevice.VirtualMidiDevice;
\r
39 import camidion.chordhelper.midieditor.DefaultMidiChannelComboBoxModel;
\r
40 import camidion.chordhelper.midieditor.MidiChannelButtonSelecter;
\r
41 import camidion.chordhelper.music.Chord;
\r
42 import camidion.chordhelper.music.Key;
\r
43 import camidion.chordhelper.music.MIDISpec;
\r
44 import camidion.chordhelper.music.Music;
\r
47 * Piano Keyboard class for MIDI Chord Helper
\r
50 * Copyright (C) 2004-2013 Akiyoshi Kamide
\r
51 * http://www.yk.rim.or.jp/~kamide/music/chordhelper/
\r
53 public class PianoKeyboard extends JComponent {
\r
57 public static final int MIN_OCTAVES = 3;
\r
61 public static final int MAX_OCTAVES = MIDISpec.MAX_NOTE_NO / 12 + 1;
\r
65 public static final Color DARK_PINK = new Color(0xFF,0x50,0x80);
\r
68 private static float WIDTH_PER_OCTAVE = Music.SEMITONES_PER_OCTAVE * 10;
\r
70 private Dimension whiteKeySize;
\r
72 private Dimension blackKeySize;
\r
74 boolean isDark = false;
\r
77 private PianoKey[] keys;
\r
79 private PianoKey[] blackKeys;
\r
81 private PianoKey[] whiteKeys;
\r
85 public BoundedRangeModel octaveRangeModel;
\r
89 public BoundedRangeModel octaveSizeModel;
\r
93 public BoundedRangeModel velocityModel = new DefaultBoundedRangeModel(64, 0, 0, 127);
\r
95 * MIDIチャンネル選択コンボボックスモデル
\r
97 public DefaultMidiChannelComboBoxModel
\r
98 midiChComboboxModel = new DefaultMidiChannelComboBoxModel();
\r
101 * ノートのリスト。配列の要素として使えるようクラス名を割り当てます。
\r
103 private class NoteList extends LinkedList<Integer> {
\r
107 * 選択マーク●がついている鍵を表すノートリスト
\r
109 private NoteList selectedKeyNoteList = new NoteList();
\r
113 private Key keySignature = null;
\r
117 private Chord chord = null;
\r
121 public ChordMatrix chordMatrix;
\r
125 public ChordDisplayLabel chordDisplay;
\r
129 public AnoGakkiPane anoGakkiPane;
\r
133 public MidiChannelButtonSelecter midiChannelButtonSelecter;
\r
135 private NoteList[] channelNotes = new NoteList[MIDISpec.MAX_CHANNELS];
\r
136 private int[] pitchBendValues = new int[MIDISpec.MAX_CHANNELS];
\r
137 private int[] pitchBendSensitivities = new int[MIDISpec.MAX_CHANNELS];
\r
138 private int[] modulations = new int[MIDISpec.MAX_CHANNELS];
\r
140 private class MidiChannelStatus extends AbstractMidiChannelStatus {
\r
141 public MidiChannelStatus(int channel) {
\r
143 channelNotes[channel] = new NoteList();
\r
144 pitchBendSensitivities[channel] = 2; // Default is wholetone = 2 semitones
\r
147 public void fireRpnChanged() {
\r
148 if( dataFor != DATA_FOR_RPN ) return;
\r
150 // RPN (MSB) - Accept 0x00 only
\r
151 if( controllerValues[0x65] != 0x00 ) return;
\r
154 switch( controllerValues[0x64] ) {
\r
155 case 0x00: // Pitch Bend Sensitivity
\r
156 if( controllerValues[0x06] == 0 ) return;
\r
157 pitchBendSensitivities[channel] = controllerValues[0x06];
\r
162 public void noteOff(int noteNumber, int velocity) {
\r
163 noteOff(noteNumber);
\r
166 public void noteOff(int noteNumber) {
\r
167 keyOff( channel, noteNumber );
\r
168 if( chordMatrix != null ) {
\r
169 if( ! isRhythmPart() )
\r
170 chordMatrix.note(false, noteNumber);
\r
172 if( midiChannelButtonSelecter != null ) {
\r
173 midiChannelButtonSelecter.repaint();
\r
177 public void noteOn(int noteNumber, int velocity) {
\r
178 if( velocity <= 0 ) {
\r
179 noteOff(noteNumber); return;
\r
181 keyOn( channel, noteNumber );
\r
182 if( midiChComboboxModel.getSelectedChannel() == channel ) {
\r
183 if( chordDisplay != null ) {
\r
184 if( chordMatrix != null && chordMatrix.isPlaying() )
\r
185 chordDisplay.clear();
\r
187 chordDisplay.setNote(noteNumber, isRhythmPart());
\r
189 if( anoGakkiPane != null ) {
\r
190 PianoKey piano_key = getPianoKey(noteNumber);
\r
191 if( piano_key != null )
\r
192 anoGakkiPane.start(PianoKeyboard.this, piano_key.indicator);
\r
195 if( chordMatrix != null ) {
\r
196 if( ! isRhythmPart() )
\r
197 chordMatrix.note(true, noteNumber);
\r
199 if( midiChannelButtonSelecter != null ) {
\r
200 midiChannelButtonSelecter.repaint();
\r
204 public void allNotesOff() {
\r
205 allKeysOff( channel, -1 );
\r
206 if( chordMatrix != null )
\r
207 chordMatrix.clearIndicators();
\r
210 public void setPitchBend(int bend) {
\r
211 super.setPitchBend(bend);
\r
212 pitchBendValues[channel] = bend;
\r
216 public void resetAllControllers() {
\r
217 super.resetAllControllers();
\r
219 // See also: Response to Reset All Controllers
\r
220 // http://www.midi.org/about-midi/rp15.shtml
\r
222 pitchBendValues[channel] = MIDISpec.PITCH_BEND_NONE;
\r
223 modulations[channel] = 0;
\r
227 public void controlChange(int controller, int value) {
\r
228 super.controlChange(controller,value);
\r
229 switch( controller ) {
\r
230 case 0x01: // Moduration (MSB)
\r
231 modulations[channel] = value;
\r
236 private void repaintNotes() {
\r
237 if( midiChComboboxModel.getSelectedChannel() != channel
\r
238 || channelNotes[channel] == null
\r
241 if( channelNotes[channel].size() > 0 || selectedKeyNoteList.size() > 0 )
\r
246 * この鍵盤の仮想MIDIデバイスです。
\r
247 * ノートオンなどのMIDIメッセージを受け取り、画面に反映します。
\r
249 public VirtualMidiDevice midiDevice = new AbstractVirtualMidiDevice() {
\r
250 class MyInfo extends Info {
\r
251 protected MyInfo() {
\r
252 super("MIDI Keyboard","Unknown vendor","Software MIDI keyboard","");
\r
258 protected MyInfo info;
\r
260 public Info getDeviceInfo() { return info; }
\r
262 info = new MyInfo();
\r
263 // 受信してMIDIチャンネルの状態を管理する
\r
265 new AbstractMidiStatus() {{
\r
266 for( int i=0; i<MIDISpec.MAX_CHANNELS; i++ )
\r
267 add(new MidiChannelStatus(i));
\r
274 * 現在選択中のMIDIチャンネルを返します。
\r
275 * @return 現在選択中のMIDIチャンネル
\r
277 public MidiChannel getSelectedChannel() {
\r
278 return midiDevice.getChannels()[midiChComboboxModel.getSelectedChannel()];
\r
281 * 現在選択中のMIDIチャンネルにノートオンメッセージを送出します。
\r
282 * ベロシティ値は現在画面で設定中の値となります。
\r
283 * @param noteNumber ノート番号
\r
285 public void noteOn(int noteNumber) {
\r
286 getSelectedChannel().noteOn(noteNumber, velocityModel.getValue());
\r
289 * 現在選択中のMIDIチャンネルにノートオフメッセージを送出します。
\r
290 * ベロシティ値は現在画面で設定中の値となります。
\r
291 * @param noteNumber ノート番号
\r
293 public void noteOff(int noteNumber) {
\r
294 getSelectedChannel().noteOff(noteNumber, velocityModel.getValue());
\r
300 private class PianoKey extends Rectangle {
\r
301 private boolean isBlack = false;
\r
302 private int position = 0;
\r
303 private String bindedKeyChar = null;
\r
304 private Rectangle indicator;
\r
305 private boolean outOfBounds = false;
\r
306 public PianoKey(Point p, Dimension d, Dimension indicatorSize) {
\r
308 Point indicatorPosition = new Point(
\r
309 p.x + (d.width - indicatorSize.width) / 2,
\r
310 p.y + d.height - indicatorSize.height - indicatorSize.height / 2 + 2
\r
312 indicator = new Rectangle(indicatorPosition, indicatorSize);
\r
314 int getNote(int chromaticOffset) {
\r
315 int n = position + chromaticOffset;
\r
316 return (outOfBounds = ( n > MIDISpec.MAX_NOTE_NO )) ? -1 : n;
\r
318 boolean paintKey(Graphics2D g2, boolean isPressed) {
\r
319 if(outOfBounds) return false;
\r
320 g2.fill3DRect(x, y, width, height, !isPressed);
\r
323 boolean paintKey(Graphics2D g2) {
\r
324 return paintKey(g2,false);
\r
326 boolean paintKeyBinding(Graphics2D g2) {
\r
327 if( bindedKeyChar == null ) return false;
\r
328 g2.drawString( bindedKeyChar, x + width/3, indicator.y - 2 );
\r
331 boolean paintIndicator(Graphics2D g2, boolean is_small, int pitch_bend_value) {
\r
334 indicator.x + indicator.width/4,
\r
335 indicator.y + indicator.height/4 + 1,
\r
341 int current_channel = midiChComboboxModel.getSelectedChannel();
\r
342 int sens = pitchBendSensitivities[current_channel];
\r
347 7 * whiteKeySize.width * sens * (pitch_bend_value - MIDISpec.PITCH_BEND_NONE)
\r
349 int additional_height = indicator.height * modulations[current_channel] / 256 ;
\r
350 int y_offset = additional_height / 2 ;
\r
352 indicator.x + ( x_offset < 0 ? x_offset : 0 ),
\r
353 indicator.y - y_offset,
\r
354 indicator.width + ( x_offset < 0 ? -x_offset : x_offset ),
\r
355 indicator.height + additional_height
\r
360 boolean paintIndicator(Graphics2D g2, boolean is_small) {
\r
361 return paintIndicator( g2, is_small, 0 );
\r
365 private class MouseKeyListener
\r
366 implements MouseListener, MouseMotionListener, KeyListener
\r
368 private void pressed(int c, int n, InputEvent e) {
\r
371 firePianoKeyPressed(n,e);
\r
373 private void released(int c, int n, InputEvent e) {
\r
376 firePianoKeyReleased(n,e);
\r
379 public void mousePressed(MouseEvent e) {
\r
380 int n = getNote(e.getPoint());
\r
381 if( n < 0 ) return;
\r
382 int c = midiChComboboxModel.getSelectedChannel();
\r
383 if( channelNotes[c].contains(n) ) return;
\r
386 requestFocusInWindow();
\r
390 public void mouseReleased(MouseEvent e) {
\r
391 int c = midiChComboboxModel.getSelectedChannel();
\r
392 NoteList nl = channelNotes[c];
\r
393 if( ! nl.isEmpty() )
\r
394 released(c, nl.poll(), e);
\r
397 public void mouseEntered(MouseEvent e) {
\r
400 public void mouseExited(MouseEvent e) {
\r
403 public void mouseDragged(MouseEvent e) {
\r
404 int n = getNote(e.getPoint());
\r
405 if( n < 0 ) return;
\r
406 int c = midiChComboboxModel.getSelectedChannel();
\r
407 NoteList nl = channelNotes[c];
\r
408 if( nl.contains(n) ) return;
\r
409 if( ! nl.isEmpty() )
\r
410 released(c, nl.poll(), e);
\r
414 public void mouseMoved(MouseEvent e) {
\r
417 public void mouseClicked(MouseEvent e) {
\r
420 public void keyPressed(KeyEvent e) {
\r
421 int kc = e.getKeyCode();
\r
422 if( kc == KeyEvent.VK_LEFT || kc == KeyEvent.VK_KP_LEFT ) {
\r
423 octaveRangeModel.setValue( octaveRangeModel.getValue() - 1 );
\r
426 else if( kc == KeyEvent.VK_RIGHT || kc == KeyEvent.VK_KP_RIGHT ) {
\r
427 octaveRangeModel.setValue( octaveRangeModel.getValue() + 1 );
\r
430 int n = getNote(e); if( n < 0 ) return;
\r
431 int c = midiChComboboxModel.getSelectedChannel();
\r
432 if( channelNotes[c].contains(n) ) return;
\r
437 public void keyReleased(KeyEvent e) {
\r
438 int c = midiChComboboxModel.getSelectedChannel();
\r
439 int n = getNote(e);
\r
440 if( n < 0 || ! channelNotes[c].contains(n) ) return;
\r
444 public void keyTyped(KeyEvent e) {
\r
449 * 新しいピアノキーボードを構築します。
\r
451 public PianoKeyboard() {
\r
452 setLayout(new BorderLayout());
\r
453 setFocusable(true);
\r
454 addFocusListener(new FocusListener() {
\r
455 public void focusGained(FocusEvent e) { repaint(); }
\r
456 public void focusLost(FocusEvent e) { repaint(); }
\r
458 MouseKeyListener mkl = new MouseKeyListener();
\r
459 addMouseListener(mkl);
\r
460 addMouseMotionListener(mkl);
\r
461 addKeyListener(mkl);
\r
462 int octaves = getPerferredOctaves();
\r
463 octaveSizeModel = new DefaultBoundedRangeModel(
\r
464 octaves, 0, MIN_OCTAVES, MAX_OCTAVES
\r
466 addChangeListener(new ChangeListener() {
\r
467 public void stateChanged(ChangeEvent e) {
\r
468 fireOctaveResized(e);
\r
469 octaveSizeChanged();
\r
473 octaveRangeModel = new DefaultBoundedRangeModel(
\r
474 (MAX_OCTAVES - octaves) / 2, octaves, 0, MAX_OCTAVES
\r
476 addChangeListener(new ChangeListener() {
\r
477 public void stateChanged(ChangeEvent e) {
\r
478 fireOctaveMoved(e);
\r
479 checkOutOfBounds();
\r
484 addComponentListener(new ComponentAdapter() {
\r
486 public void componentResized(ComponentEvent e) {
\r
487 octaveSizeModel.setValue( getPerferredOctaves() );
\r
488 octaveSizeChanged();
\r
491 midiChComboboxModel.addListDataListener(
\r
492 new ListDataListener() {
\r
493 public void contentsChanged(ListDataEvent e) {
\r
494 int c = midiChComboboxModel.getSelectedChannel();
\r
495 for( int n : channelNotes[c] )
\r
496 if( autoScroll(n) ) break;
\r
499 public void intervalAdded(ListDataEvent e) {}
\r
500 public void intervalRemoved(ListDataEvent e) {}
\r
504 public void paint(Graphics g) {
\r
505 if( keys == null ) return;
\r
506 Graphics2D g2 = (Graphics2D) g;
\r
507 Dimension d = getSize();
\r
510 g2.setBackground( getBackground() );
\r
511 g2.clearRect( 0, 0, d.width, d.height );
\r
514 g2.setColor( isDark ? Color.gray : Color.white );
\r
515 for( PianoKey k : whiteKeys ) k.paintKey(g2);
\r
517 NoteList notesArray[] = {
\r
518 (NoteList)selectedKeyNoteList.clone(),
\r
519 (NoteList)channelNotes[midiChComboboxModel.getSelectedChannel()].clone()
\r
523 // ノートオン状態の白鍵を塗り重ねる
\r
524 for( int n : notesArray[1] )
\r
525 if( (key=getPianoKey(n)) != null && !(key.isBlack) )
\r
526 key.paintKey(g2,true);
\r
529 g2.setColor(getForeground());
\r
530 for( PianoKey k : blackKeys ) k.paintKey(g2);
\r
532 // ノートオン状態の黒鍵を塗り重ねる
\r
533 g2.setColor( Color.gray );
\r
534 for( int n : notesArray[1] )
\r
535 if( (key=getPianoKey(n)) != null && key.isBlack )
\r
536 key.paintKey(g2,true);
\r
539 for( NoteList nl : notesArray ) {
\r
540 if( nl == null ) continue;
\r
541 for( Integer ni : nl ) {
\r
542 if( ni == null ) continue;
\r
544 if( (key=getPianoKey(n)) == null ) continue;
\r
545 boolean isOnScale = (keySignature == null || keySignature.isOnScale(n));
\r
547 if( chord != null && (chordIndex = chord.indexOf(n)) >=0 ) {
\r
548 g2.setColor(Chord.NOTE_INDEX_COLORS[chordIndex]);
\r
551 g2.setColor(isDark && isOnScale ? Color.pink : DARK_PINK);
\r
553 int c = midiChComboboxModel.getSelectedChannel();
\r
554 key.paintIndicator(g2, false, pitchBendValues[c]);
\r
555 if( ! isOnScale ) {
\r
556 g2.setColor(Color.white);
\r
557 key.paintIndicator(g2, true);
\r
561 if( isFocusOwner() ) {
\r
562 // Show PC-key binding
\r
563 for( PianoKey k : bindedKeys ) {
\r
565 k.isBlack ? Color.gray.brighter() :
\r
566 isDark ? getForeground() :
\r
567 getForeground().brighter()
\r
569 k.paintKeyBinding(g2);
\r
574 protected void firePianoKeyPressed(int note_no, InputEvent event) {
\r
575 Object[] listeners = listenerList.getListenerList();
\r
576 for (int i = listeners.length-2; i>=0; i-=2) {
\r
577 if (listeners[i]==PianoKeyboardListener.class) {
\r
578 ((PianoKeyboardListener)listeners[i+1]).pianoKeyPressed(note_no,event);
\r
582 protected void firePianoKeyReleased(int note_no, InputEvent event) {
\r
583 Object[] listeners = listenerList.getListenerList();
\r
584 for (int i = listeners.length-2; i>=0; i-=2) {
\r
585 if (listeners[i]==PianoKeyboardListener.class) {
\r
586 ((PianoKeyboardListener)listeners[i+1]).pianoKeyReleased(note_no,event);
\r
590 protected void fireOctaveMoved(ChangeEvent event) {
\r
591 Object[] listeners = listenerList.getListenerList();
\r
592 for (int i = listeners.length-2; i>=0; i-=2) {
\r
593 if (listeners[i]==PianoKeyboardListener.class) {
\r
594 ((PianoKeyboardListener)listeners[i+1]).octaveMoved(event);
\r
598 protected void fireOctaveResized(ChangeEvent event) {
\r
599 Object[] listeners = listenerList.getListenerList();
\r
600 for (int i = listeners.length-2; i>=0; i-=2) {
\r
601 if (listeners[i]==PianoKeyboardListener.class) {
\r
602 ((PianoKeyboardListener)listeners[i+1]).octaveResized(event);
\r
608 * 指定のノート番号に対する1個のピアノキーを返します。
\r
609 * @param noteNumber ノート番号
\r
610 * @return ピアノキー(範囲外の場合 null)
\r
612 private PianoKey getPianoKey(int noteNumber) {
\r
613 int i = noteNumber - octaveRangeModel.getValue() * 12 ;
\r
614 return i>=0 && i<keys.length ? keys[i]: null;
\r
617 * 指定の座標におけるノート番号を返します。
\r
619 * @return ノート番号(範囲外の場合 -1)
\r
621 private int getNote(Point point) {
\r
622 PianoKey k = getPianoKeyAt(point);
\r
623 return k==null ? -1 : k.getNote(getChromaticOffset());
\r
626 * 指定の座標における1個のピアノキーを返します。
\r
628 * @return ピアノキー(範囲外の場合 null)
\r
630 private PianoKey getPianoKeyAt(Point point) {
\r
631 int indexWhite = point.x / whiteKeySize.width;
\r
632 int indexOctave = indexWhite / 7;
\r
633 int i = (indexWhite -= indexOctave * 7) * 2 + indexOctave * 12;
\r
634 if( indexWhite >= 3 ) i--;
\r
635 if( i < 0 || i > keys.length-1 )
\r
637 if( point.y > blackKeySize.height )
\r
642 if( k.isBlack && !(k.outOfBounds) && k.contains(point) )
\r
645 if( i < keys.length-1 ) {
\r
647 if( k.isBlack && !(k.outOfBounds) && k.contains(point) )
\r
653 private PianoKey[] bindedKeys;
\r
654 private int bindedKeyPosition;
\r
655 private String bindedKeyChars;
\r
656 private PianoKey getPianoKey(KeyEvent e) {
\r
657 int i = bindedKeyChars.indexOf(e.getKeyChar());
\r
658 return i >= 0 ? keys[bindedKeyPosition + i] : null;
\r
660 private int getNote(KeyEvent e) {
\r
661 PianoKey k = getPianoKey(e);
\r
662 return k==null ? -1 : k.getNote(getChromaticOffset());
\r
664 private void changeKeyBinding(int from, String keyChars) {
\r
666 bindedKeys = new PianoKey[(bindedKeyChars = keyChars).length()];
\r
667 bindedKeyPosition = from;
\r
668 for( int i = 0; i < bindedKeyChars.length(); i++ ) {
\r
669 bindedKeys[i] = k = keys[ bindedKeyPosition + i ];
\r
670 k.bindedKeyChar = bindedKeyChars.substring( i, i+1 );
\r
675 private void checkOutOfBounds() {
\r
676 if( keys == null ) return;
\r
677 for( PianoKey k : keys ) k.getNote(getChromaticOffset());
\r
679 private void keyOff(int ch, int noteNumber) {
\r
680 if( noteNumber < 0 || ch < 0 || ch >= channelNotes.length ) return;
\r
681 channelNotes[ch].remove((Object)noteNumber);
\r
682 if( ch == midiChComboboxModel.getSelectedChannel() )
\r
685 private void keyOn(int ch, int noteNumber) {
\r
686 if( noteNumber < 0 || ch < 0 || ch >= channelNotes.length ) return;
\r
687 channelNotes[ch].add(noteNumber);
\r
688 setSelectedNote(ch,noteNumber);
\r
690 public boolean autoScroll(int noteNumber) {
\r
691 if( octaveRangeModel == null || keys == null )
\r
693 int i = noteNumber - getChromaticOffset();
\r
695 octaveRangeModel.setValue(
\r
696 octaveRangeModel.getValue() - (-i)/Music.SEMITONES_PER_OCTAVE - 1
\r
700 if( i >= keys.length ) {
\r
701 octaveRangeModel.setValue(
\r
702 octaveRangeModel.getValue() + (i-keys.length)/Music.SEMITONES_PER_OCTAVE + 1
\r
708 public void addPianoKeyboardListener(PianoKeyboardListener l) {
\r
709 listenerList.add(PianoKeyboardListener.class, l);
\r
711 public void removePianoKeyboardListener(PianoKeyboardListener l) {
\r
712 listenerList.remove(PianoKeyboardListener.class, l);
\r
715 return channelNotes[midiChComboboxModel.getSelectedChannel()].size();
\r
717 public int countKeyOn(int ch) {
\r
718 return channelNotes[ch].size();
\r
720 void allKeysOff(int ch, int numMarks) {
\r
721 if( ! selectedKeyNoteList.isEmpty() ) return;
\r
724 selectedKeyNoteList = (NoteList)(channelNotes[ch].clone());
\r
727 selectedKeyNoteList.add(
\r
728 channelNotes[ch].get(channelNotes[ch].size()-1)
\r
733 channelNotes[ch].clear();
\r
734 if( midiChComboboxModel.getSelectedChannel() == ch )
\r
737 public void clear() {
\r
738 selectedKeyNoteList.clear();
\r
739 channelNotes[midiChComboboxModel.getSelectedChannel()].clear();
\r
744 int current_channel = midiChComboboxModel.getSelectedChannel();
\r
745 switch( channelNotes[current_channel].size() ) {
\r
746 case 1: return channelNotes[current_channel].get(0);
\r
748 if( selectedKeyNoteList.size() == 1 )
\r
749 return selectedKeyNoteList.get(0);
\r
755 public void setSelectedNote(int noteNumber) {
\r
756 setSelectedNote(midiChComboboxModel.getSelectedChannel(), noteNumber);
\r
758 void setSelectedNote(int ch, int note_no) {
\r
759 if( ch != midiChComboboxModel.getSelectedChannel() )
\r
761 selectedKeyNoteList.add(note_no);
\r
762 int maxSel = (chord == null ? maxSelectable : chord.numberOfNotes());
\r
763 while( selectedKeyNoteList.size() > maxSel )
\r
764 selectedKeyNoteList.poll();
\r
765 if( !autoScroll(note_no) ) {
\r
766 // When autoScroll() returned false, stateChanged() not invoked - need repaint()
\r
770 public Integer[] getSelectedNotes() {
\r
771 return selectedKeyNoteList.toArray(new Integer[0]);
\r
773 Chord getChord() { return chord; }
\r
774 public void setChord(Chord c) {
\r
775 chordDisplay.setChord(chord = c);
\r
777 public void setKeySignature(Key ks) {
\r
781 private int maxSelectable = 1;
\r
782 public void setMaxSelectable( int maxSelectable ) {
\r
783 this.maxSelectable = maxSelectable;
\r
785 int getMaxSelectable() { return maxSelectable; }
\r
786 public int getChromaticOffset() {
\r
787 return octaveRangeModel.getValue() * Music.SEMITONES_PER_OCTAVE ;
\r
789 public int getOctaves() { return octaveSizeModel.getValue(); }
\r
790 private int getPerferredOctaves() {
\r
791 int octaves = Math.round( (float)getWidth() / WIDTH_PER_OCTAVE );
\r
792 if( octaves > MAX_OCTAVES ) {
\r
793 octaves = MAX_OCTAVES;
\r
795 else if( octaves < MIN_OCTAVES ) {
\r
796 octaves = MIN_OCTAVES;
\r
800 private void octaveSizeChanged() {
\r
801 int octaves = octaveSizeModel.getValue();
\r
802 String defaultBindedKeyChars = "zsxdcvgbhnjm,l.;/\\]";
\r
803 Dimension keyboard_size = getSize();
\r
804 if( keyboard_size.width == 0 ) {
\r
807 whiteKeySize = new Dimension(
\r
808 (keyboard_size.width - 1) / (octaves * 7 + 1),
\r
809 keyboard_size.height - 1
\r
811 blackKeySize = new Dimension(
\r
812 whiteKeySize.width * 3 / 4,
\r
813 whiteKeySize.height * 3 / 5
\r
815 Dimension indicatorSize = new Dimension(
\r
816 whiteKeySize.width / 2,
\r
817 whiteKeySize.height / 6
\r
819 octaveRangeModel.setExtent( octaves );
\r
820 octaveRangeModel.setValue( (MAX_OCTAVES - octaves) / 2 );
\r
821 WIDTH_PER_OCTAVE = keyboard_size.width / octaves;
\r
823 // Construct piano-keys
\r
825 keys = new PianoKey[ octaves * 12 + 1 ];
\r
826 Vector<PianoKey> vBlackKeys = new Vector<PianoKey>();
\r
827 Vector<PianoKey> vWhiteKeys = new Vector<PianoKey>();
\r
828 Point keyPoint = new Point(1,1);
\r
831 boolean is_CDE = true;
\r
832 for( i = i12 = 0; i < keys.length; i++, i12++ ) {
\r
834 case 12: is_CDE = true; i12 = 0; break;
\r
835 case 5: is_CDE = false; break;
\r
838 keyPoint.x = whiteKeySize.width * (
\r
839 i / Music.SEMITONES_PER_OCTAVE * 7 + (i12+(is_CDE?1:2))/2
\r
841 if( Music.isOnScale(i12,0) ) {
\r
842 k = new PianoKey( keyPoint, whiteKeySize, indicatorSize );
\r
847 keyPoint.x -= ( (is_CDE?5:12) - i12 )/2 * blackKeySize.width / (is_CDE?3:4);
\r
848 k = new PianoKey( keyPoint, blackKeySize, indicatorSize );
\r
852 (keys[i] = k).position = i;
\r
854 whiteKeys = vWhiteKeys.toArray(new PianoKey[1]);
\r
855 blackKeys = vBlackKeys.toArray(new PianoKey[1]);
\r
856 changeKeyBinding(((octaves - 1) / 2) * 12, defaultBindedKeyChars);
\r
857 checkOutOfBounds();
\r
860 void setDarkMode(boolean isDark) {
\r
861 this.isDark = isDark;
\r
862 setBackground( isDark ? Color.black : null );
\r