/* * Copyright (C) 2017 たんらる */ package fourthline.mabiicco.ui.editor; import java.awt.BorderLayout; import java.awt.Component; import java.awt.Frame; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; import java.util.ArrayList; import java.util.Iterator; import java.util.function.IntConsumer; import javax.sound.midi.MidiDevice; import javax.sound.midi.MidiMessage; import javax.sound.midi.MidiSystem; import javax.sound.midi.MidiUnavailableException; import javax.sound.midi.Receiver; import javax.sound.midi.Sequencer; import javax.sound.midi.ShortMessage; import javax.sound.midi.Transmitter; import javax.swing.BoxLayout; import javax.swing.ButtonGroup; import javax.swing.JComboBox; import javax.swing.JDialog; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JRadioButton; import javax.swing.JSpinner; import javax.swing.JTextField; import javax.swing.SpinnerModel; import fourthline.mabiicco.MabiIccoProperties; import fourthline.mabiicco.midi.IPlayNote; import fourthline.mabiicco.midi.InstClass; import fourthline.mabiicco.midi.MabiDLS; import fourthline.mabiicco.ui.IMMLManager; import fourthline.mabiicco.ui.PianoRollView; import fourthline.mmlTools.MMLEventList; import fourthline.mmlTools.MMLNoteEvent; import fourthline.mmlTools.MMLTrack; import fourthline.mmlTools.parser.MMLEventParser; import static fourthline.mabiicco.AppResource.appText; /** * キーボード入力による編集. */ public final class KeyboardEditor { private static boolean debug = false; public static void setDebug(boolean b) { debug = b; } private final JDialog dialog; private final IMMLManager mmlManager; private final IPlayNote player; private final IEditAlign editAlign; private final PianoRollView pianoRollView; private final int initOct = 4; private final int minOct = 0; private final int maxOct = 8; private final JPanel panel = new JPanel(new BorderLayout()); private final JComboBox<MidiDevice.Info> midiList = new JComboBox<>(); private final JSpinner velocityValueField = NumberSpinner.createSpinner(MMLNoteEvent.INIT_VOL, 0, MMLNoteEvent.MAX_VOL, 1); private final JSpinner octaveValueField = NumberSpinner.createSpinner(initOct, minOct, maxOct, 1); private final JRadioButton monoButton = new JRadioButton(appText("edit.midi_device.mono")); private final JRadioButton chordButton = new JRadioButton(appText("edit.midi_device.chord")); private IntConsumer noteAlignChanger; private final CharKeyboard charKeyboard = new CharKeyboard(); private final MidiKeyboard midiKeyboard = new MidiKeyboard(); /** 単音/和音入力は排他 */ private IKeyboardAction currentAction = null; private synchronized boolean tryLock(IKeyboardAction action) { if (currentAction == null) { if (debug) System.out.println("lock "+action.getClass().getSimpleName()); currentAction = action; return true; } else if (currentAction == action) { return true; } return false; } private synchronized void unlock(IKeyboardAction action) { if (currentAction != action) { throw new IllegalArgumentException("keyboardAction unlock"); } if (debug) System.out.println("unlock "+action.getClass().getSimpleName()); currentAction = null; } public boolean isEmpty() { return (currentAction == null); } public KeyboardEditor(Frame parentFrame, IMMLManager mmlManager, IPlayNote player, IEditAlign editAlign, PianoRollView pianoRollView) { this.mmlManager = mmlManager; this.player = player; this.editAlign = editAlign; this.pianoRollView = pianoRollView; dialog = new JDialog(parentFrame, appText("edit.keyboard.input"), true); initializePanel(); } public void setNoteAlignChanger(IntConsumer noteAlignChanger) { this.noteAlignChanger = noteAlignChanger; } private JPanel createLPanel(Component c) { JPanel panel = new JPanel(new BorderLayout()); panel.add(c, BorderLayout.WEST); return panel; } private void initialSpinnerProperty(JSpinner spinner) { JTextField field = ((JSpinner.NumberEditor) spinner.getEditor()).getTextField(); spinner.setFocusable(false); field.setEditable(false); field.setFocusable(false); } private void initializePanel() { JPanel panel1 = new JPanel(); panel1.add(new JLabel(appText("edit.velocity"))); initialSpinnerProperty(velocityValueField); panel1.add(velocityValueField); JPanel panel2 = new JPanel(); panel2.add(new JLabel(appText("edit.octave"))); initialSpinnerProperty(octaveValueField); panel2.add(octaveValueField); JPanel panel3 = new JPanel(); panel3.add(new JLabel(appText("edit.midi_device"))); midiList.setFocusable(false); midiList.addActionListener(new MidiEventListener()); panel3.add(midiList); JPanel panel4 = new JPanel(); panel4.add(new JLabel(appText("edit.midi_device.input_method"))); ButtonGroup buttonGroup = new ButtonGroup(); monoButton.setFocusable(false); chordButton.setFocusable(false); buttonGroup.add(monoButton); buttonGroup.add(chordButton); selectMidiModeButton(); monoButton.addActionListener(t -> changeEditor(false)); chordButton.addActionListener(t -> changeEditor(true)); panel4.add(monoButton); panel4.add(chordButton); JPanel nPanel = new JPanel(); nPanel.setLayout(new BoxLayout(nPanel, BoxLayout.Y_AXIS)); nPanel.add(createLPanel(new JLabel(appText("edit.keyboard.input.description")))); nPanel.add(createLPanel(panel1)); nPanel.add(createLPanel(panel2)); nPanel.add(createLPanel(panel3)); nPanel.add(createLPanel(panel4)); panel.add(nPanel, BorderLayout.NORTH); dialog.addKeyListener(charKeyboard); dialog.getContentPane().add(panel); dialog.setResizable(false); } private void editorInit() { currentAction = null; charKeyboard.clear(); midiKeyboard.clear(); } public void setVisible(boolean b) { if (!b) { dialog.setVisible(b); return; } String initialMidiName = MabiIccoProperties.getInstance().midiInputDevice.get(); midiList.removeAllItems(); MabiDLS.getInstance().getMidiInDevice().forEach(t -> midiList.addItem(t)); int tickOffset = (int) pianoRollView.getSequencePosition(); MMLEventList activePart = mmlManager.getActiveMMLPart(); MMLNoteEvent prevNote = activePart.searchPrevNoteOnTickOffset(tickOffset); if (prevNote == null) { velocityValueField.setValue(MMLNoteEvent.INIT_VOL); } else { velocityValueField.setValue(prevNote.getVelocity()); } // 設定値にもっているMIDIデバイスを選択する. for (int i = 0; i < midiList.getItemCount(); i++) { MidiDevice.Info info = midiList.getItemAt(i); System.out.println("midi search > \""+initialMidiName+"\" \""+info.getName()+"\""); if (info.getName().equals(initialMidiName)) { midiList.setSelectedIndex(i); break; } } editorInit(); midiKeyboard.initPartList(); dialog.pack(); dialog.setLocationRelativeTo(null); dialog.setVisible(b); } private int nextTick(int tickOffset) { int tickLength = editAlign.getEditAlign(); int nextTick = tickOffset + tickLength; return nextTick; } private int prevTick(int tickOffset) { int tickLength = editAlign.getEditAlign(); int nextTick = tickOffset - tickLength; return nextTick; } /** * MIDI入力モードを変更する. 入力中のモード変更は禁止する. * @param chordInput 単音は false, 和音は true. */ public void changeEditor(boolean chordInput) { if (currentAction == null) { MabiIccoProperties.getInstance().midiChordInput.set(chordInput); charKeyboard.clear(); midiKeyboard.clear(); } selectMidiModeButton(); } /** * MIDI入力モードに基づいてボタンの選択状態を設定する. */ private void selectMidiModeButton() { boolean chordInput = MabiIccoProperties.getInstance().midiChordInput.get(); if (monoButton.isSelected() == chordInput) { monoButton.setSelected(!chordInput); } if (chordButton.isSelected() != chordInput) { chordButton.setSelected(chordInput); } } private interface IKeyboardAction { void clear(); void pressNote(int note); void addTick(); void releaseNote(int note); boolean isPlay(); } /** * キーボード/MIDI: 単音入力 */ private final class CharKeyboard implements KeyListener, IKeyboardAction { private MMLNoteEvent editNote = null; private int playNote = Integer.MIN_VALUE; private char typeCode = 0; private int charToNote(char code) { int octave = ((Integer) octaveValueField.getValue()).intValue(); int note = MMLEventParser.firstNoteNumber("o"+octave+code); return note; } @Override public synchronized void clear() { editNote = null; playNote = Integer.MIN_VALUE; typeCode = 0; } @Override public synchronized void pressNote(int note) { MMLEventList activePart = mmlManager.getActiveMMLPart(); int velocity = ((Integer) velocityValueField.getValue()).intValue(); int tickOffset = (int) pianoRollView.getSequencePosition(); int nextTick = nextTick(tickOffset); MMLNoteEvent noteEvent = new MMLNoteEvent(note, nextTick-tickOffset, tickOffset, velocity); activePart.addMMLNoteEvent(noteEvent); playNote = note; player.playNote(note, velocity); pianoRollView.setSequenceTick(nextTick); mmlManager.updateActivePart(true); mmlManager.updatePianoRollView(note); this.editNote = noteEvent; } private synchronized void addRest() { int tickOffset = (int) pianoRollView.getSequencePosition(); int nextTick = nextTick(tickOffset); MMLEventList activePart = mmlManager.getActiveMMLPart(); MMLNoteEvent noteEvent = activePart.searchOnTickOffset(tickOffset); activePart.deleteMMLEvent(noteEvent); pianoRollView.setSequenceTick(nextTick); mmlManager.updateActivePart(true); mmlManager.updatePianoRollView(); this.editNote = null; } private void backDelete() { int tickOffset = (int) pianoRollView.getSequencePosition(); MMLEventList activePart = mmlManager.getActiveMMLPart(); int prevTick = prevTick(tickOffset); MMLNoteEvent deleteEvent = activePart.searchOnTickOffset(prevTick); activePart.deleteMMLEvent(deleteEvent); pianoRollView.setSequenceTick(prevTick); mmlManager.updateActivePart(true); MMLNoteEvent prevNote = activePart.searchPrevNoteOnTickOffset(prevTick); if (prevNote != null) { mmlManager.updatePianoRollView(prevNote.getNote()); } else { mmlManager.updatePianoRollView(); } this.editNote = null; } private void octaveChange(char code) { SpinnerModel model = octaveValueField.getModel(); try { if (code == '<') { octaveValueField.setValue( model.getPreviousValue() ); } else if (code == '>') { octaveValueField.setValue( model.getNextValue() ); } } catch (IllegalArgumentException e) {} this.editNote = null; } private void addSharpFlat(char code) { if (editNote == null) { return; } if ( (code == '+') || (code == '#') ) { int note = editNote.getNote(); if (note < 107) { note++; } editNote.setNote(note); } else if (code == '-') { int note = editNote.getNote(); if (note >= 0) { note--; } editNote.setNote(note); } else { return; } int note = editNote.getNote(); playNote = note; player.playNote(note, editNote.getVelocity()); mmlManager.updateActivePart(true); mmlManager.updatePianoRollView(editNote.getNote()); } @Override public synchronized void addTick() { int tickOffset = (int) pianoRollView.getSequencePosition(); MMLEventList activePart = mmlManager.getActiveMMLPart(); if (editNote == null) { return; } int nextTick = nextTick(tickOffset); editNote.setTick(nextTick - editNote.getTickOffset()); activePart.addMMLNoteEvent(editNote); pianoRollView.setSequenceTick(nextTick); mmlManager.updateActivePart(true); mmlManager.updatePianoRollView(editNote.getNote()); } private void cursorMove(boolean forward) { int tickOffset = (int) pianoRollView.getSequencePosition(); MMLEventList activePart = mmlManager.getActiveMMLPart(); int nextTick = forward ? nextTick(tickOffset) : prevTick(tickOffset); pianoRollView.setSequenceTick(nextTick); MMLNoteEvent prevNote = activePart.searchPrevNoteOnTickOffset(nextTick); if (prevNote != null) { mmlManager.updatePianoRollView(prevNote.getNote()); } else { mmlManager.updatePianoRollView(); } this.editNote = null; } private void velocityChange(boolean up) { SpinnerModel model = velocityValueField.getModel(); try { velocityValueField.setValue( up ? model.getNextValue() : model.getPreviousValue() ); } catch (IllegalArgumentException e) {} } private void changeNoteAlign(char code) { char select[] = { '1', '2', '3', '4', '5', '6', '7', '8', '9', '0' }; for (int i = 0; i < select.length; i++) { if (select[i] == code) { noteAlignChanger.accept(i); } } } private void pressAction(char code) { if (!tryLock(this)) { return; } if ( (code >= 'a') && (code <= 'g') || (code >= 'A') && (code <= 'G') ) { int note = charToNote(code); if (playNote != note) { pressNote(note); } } else if ( (code == 'r') || (code == 'R') ) { addRest(); } else if (code == KeyEvent.VK_BACK_SPACE) { backDelete(); } else if ( (code == '<') || (code == '>') ) { octaveChange(code); } else if ( (code == '+') || (code == '#') || (code == '-') ) { addSharpFlat(code); } else if (code == KeyEvent.VK_ESCAPE) { dialog.setVisible(false); } else if (code == KeyEvent.VK_LEFT) { cursorMove(false); } else if (code == KeyEvent.VK_RIGHT) { cursorMove(true); } else if (code == KeyEvent.VK_DOWN) { velocityChange(false); } else if (code == KeyEvent.VK_UP) { velocityChange(true); } else if ( (code >= '0') && (code <= '9') ) { changeNoteAlign(code); } if (!isPlay()) { unlock(this); } } @Override public boolean isPlay() { return (playNote != Integer.MIN_VALUE); } @Override public synchronized void releaseNote(int note) { if ( (note != Integer.MIN_VALUE) && (playNote == note) ) { playNote = Integer.MIN_VALUE; player.offNote(); } } @Override public void keyTyped(KeyEvent e) { Sequencer sequencer = MabiDLS.getInstance().getSequencer(); synchronized (this) { if (sequencer.isRunning()) { return; } char code = e.getKeyChar(); // スペースキーは現在の入力に対して実施する. if (code == ' ') { if (currentAction != null) { currentAction.addTick(); } return; } if (typeCode != code) { typeCode = code; pressAction(code); } } } @Override public void keyPressed(KeyEvent e) { if (e.getKeyChar() == ' ') { return; } int code = e.getKeyCode(); if ( (code == KeyEvent.VK_LEFT) || (code == KeyEvent.VK_RIGHT) || (code == KeyEvent.VK_UP) || (code == KeyEvent.VK_DOWN)) { pressAction((char)code); } } @Override public void keyReleased(KeyEvent e) { synchronized (this) { char keyChar = e.getKeyChar(); if ( typeCode != keyChar ) { return; } typeCode = 0; if ( (keyChar != ' ') && (playNote != Integer.MIN_VALUE) ) { if (tryLock(this)) { unlock(this); } } releaseNote(playNote); } } } /** * MIDI: 和音入力 */ private final class MidiKeyboard implements IKeyboardAction { private final ArrayList<MMLNoteEvent> chord = new ArrayList<>(); private final ArrayList<MMLEventList> partList = new ArrayList<>(); private void initPartList() { partList.clear(); int program = mmlManager.getActivePartProgram(); boolean enablePart[] = InstClass.getEnablePartByProgram(program); int trackIndex = mmlManager.getActiveTrackIndex(); MMLTrack activeTrack = mmlManager.getMMLScore().getTrack(trackIndex); for (int i = 0; i < enablePart.length; i++) { if (enablePart[i]) { partList.add(activeTrack.getMMLEventAtIndex(i)); } } } @Override public synchronized void clear() { chord.clear(); } @Override public synchronized void pressNote(int note) { if (chord.size() >= partList.size()) { return; } int tickOffset = (int) pianoRollView.getSequencePosition(); int tickLength = editAlign.getEditAlign(); if (!chord.isEmpty()) { tickLength = chord.get(0).getTick(); } int velocity = ((Integer)velocityValueField.getValue()).intValue(); if (!chord.stream().anyMatch(t -> t.getNote() == note)) { MMLNoteEvent addNoteEvent = new MMLNoteEvent(note, tickLength, tickOffset, velocity); chord.add(addNoteEvent); } Iterator<MMLNoteEvent> noteList = chord.iterator(); for (int i = 0; i < partList.size(); i++) { if (noteList.hasNext()) { partList.get(i).addMMLNoteEvent( noteList.next() ); } else { MMLNoteEvent deleteNoteEvent = partList.get(i).searchOnTickOffset(tickOffset); if (deleteNoteEvent != null) { partList.get(i).deleteMMLEvent(deleteNoteEvent); } } } int playNote[] = new int[chord.size()]; for (int i = 0; i < playNote.length; i++) { playNote[i] = chord.get(i).getNote(); } player.playNote(playNote, velocity); mmlManager.updateActivePart(true); mmlManager.updatePianoRollView(note); } @Override public synchronized void addTick() { for (int i = 0; i < chord.size(); i++) { MMLNoteEvent note = chord.get(i); partList.get(i).deleteMMLEvent(note); note.setTick( note.getTick() + editAlign.getEditAlign() ); partList.get(i).addMMLNoteEvent(note); } mmlManager.updateActivePart(true); mmlManager.updatePianoRollView(); } @Override public synchronized void releaseNote(int note) { if (chord.size() == 0) { return; } int nextTick = chord.get(0).getEndTick(); for (MMLNoteEvent n : chord) { if (n.getNote() == note) { chord.remove(n); break; } } if (chord.size() == 0) { player.offNote(); pianoRollView.setSequenceTick(nextTick); mmlManager.updateActivePart(true); mmlManager.updatePianoRollView(); } } @Override public boolean isPlay() { return (chord.size() != 0); } } /** * MIDIデバイスからのイベント処理を行います. */ private final class MidiEventListener implements Receiver, ActionListener { private MidiDevice midiDevice = null; @Override public void actionPerformed(ActionEvent event) { if (event.getSource() != midiList) { return; } if (midiDevice != null) { midiDevice.close(); } MidiDevice.Info info = midiList.getItemAt(midiList.getSelectedIndex()); if (info == null) return; System.out.println("midi select > \""+info.getName()+"\""); try { MidiDevice device = MidiSystem.getMidiDevice(info); if (!device.isOpen()) { device.open(); Transmitter transmitter = MidiSystem.getMidiDevice(info).getTransmitter(); transmitter.setReceiver(this); midiDevice = device; } MabiIccoProperties.getInstance().midiInputDevice.set(info.getName()); } catch (MidiUnavailableException e) { e.printStackTrace(); } } private synchronized void shortMessageAction(ShortMessage msg) { int command = msg.getCommand(); int data1 = msg.getData1(); int data2 = msg.getData2(); int note = data1 - 12; IKeyboardAction action = MabiIccoProperties.getInstance().midiChordInput.get() ? midiKeyboard : charKeyboard; if (!tryLock(action)) { return; } switch (command) { case ShortMessage.CONTROL_CHANGE: if (data1 == 64) { // Hold1 if (data2 > 0) { action.addTick(); } } break; case ShortMessage.NOTE_ON: if (data2 > 0) { action.pressNote(note); break; } // data2 == 0 は Note Off. case ShortMessage.NOTE_OFF: action.releaseNote(note); break; } if (!action.isPlay()) { unlock(action); } } @Override public void send(MidiMessage message, long timeStamp) { Sequencer sequencer = MabiDLS.getInstance().getSequencer(); if (sequencer.isRunning()) { return; } if (message.getStatus() == ShortMessage.ACTIVE_SENSING) { return; } if (message instanceof ShortMessage) { shortMessageAction((ShortMessage)message); } } @Override public void close() {} } public KeyListener getKeyListener() { return charKeyboard; } public Receiver getReciever() { ActionListener actionListener[] = midiList.getActionListeners(); for (ActionListener action : actionListener) { if (action instanceof Receiver) { return (Receiver) action; } } return null; } }