Merge remote-tracking branch 'origin/GP-4516_ryanmkurtz_PR-6337_agatti_midi-resource'

This commit is contained in:
Ryan Kurtz 2024-05-07 06:29:57 -04:00
commit f2a94605eb
6 changed files with 263 additions and 10 deletions

View file

@ -65,7 +65,7 @@ public class AudioPlayer implements Playable, LineListener {
clip.start();
}
catch (UnsupportedAudioFileException | IOException | LineUnavailableException e) {
Msg.debug(this, "Unable to play audio", e);
Msg.error(this, "Unable to play audio", e);
}
}

View file

@ -0,0 +1,145 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.program.model.data;
import java.io.DataInputStream;
import java.io.EOFException;
import ghidra.docking.settings.Settings;
import ghidra.program.model.mem.MemBuffer;
import ghidra.util.Msg;
public class MIDIDataType extends BuiltIn implements Dynamic {
public static byte[] MAGIC =
new byte[] { (byte) 'M', (byte) 'T', (byte) 'h', (byte) 'd', (byte) 0x00, (byte) 0x00,
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
(byte) 0x00, (byte) 0x00, (byte) 'M', (byte) 'T', (byte) 'r', (byte) 'k' };
public static byte[] MAGIC_MASK =
new byte[] { (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x00, (byte) 0x00,
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
(byte) 0x00, (byte) 0x00, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff };
public MIDIDataType() {
this(null);
}
public MIDIDataType(DataTypeManager dtm) {
super(null, "MIDI-Score", dtm);
}
@Override
public int getLength() {
return -1;
}
@Override
public int getLength(MemBuffer buf, int maxLength) {
try (DataInputStream stream = new DataInputStream(
buf.getInputStream(0, maxLength > 0 ? maxLength : Integer.MAX_VALUE))) {
byte[] chunkType = new byte[4];
if (stream.read(chunkType) < chunkType.length) {
throw new EOFException();
}
if (chunkType[0] != (byte) 'M' || chunkType[1] != (byte) 'T' ||
chunkType[2] != (byte) 'h' || chunkType[3] != (byte) 'd') {
return -1;
}
long chunkLength = Integer.toUnsignedLong(stream.readInt());
if (chunkLength != 6) {
throw new InvalidDataTypeException("Unexpected header length.");
}
stream.skip(2);
int tracks = Short.toUnsignedInt(stream.readShort());
stream.skip(2);
int computedLength = 14;
while (tracks > 0) {
if (stream.read(chunkType) < chunkType.length) {
throw new EOFException();
}
chunkLength = Integer.toUnsignedLong(stream.readInt());
stream.skip(chunkLength);
computedLength += 8 + chunkLength;
if (chunkType[0] != (byte) 'M' || chunkType[1] != (byte) 'T' ||
chunkType[2] != (byte) 'r' || chunkType[3] != (byte) 'k') {
continue;
}
tracks--;
}
return computedLength;
}
catch (Exception e) {
Msg.debug(this, "Invalid MIDI data at " + buf.getAddress());
return -1;
}
}
@Override
public boolean canSpecifyLength() {
return false;
}
@Override
public DataType clone(DataTypeManager dtm) {
if (dtm == getDataTypeManager()) {
return this;
}
return new MIDIDataType(dtm);
}
@Override
public String getDescription() {
return "MIDI score stored within program";
}
@Override
public String getMnemonic(Settings settings) {
return "MIDI";
}
@Override
public String getRepresentation(MemBuffer buf, Settings settings, int length) {
return "<MIDI-Resource>";
}
@Override
public Object getValue(MemBuffer buf, Settings settings, int length) {
byte[] data = new byte[length];
if (buf.getBytes(data, 0) != length) {
Msg.error(this, "MIDI-Score error: Not enough bytes!");
return null;
}
return new ScorePlayer(data);
}
@Override
public Class<?> getValueClass(Settings settings) {
return ScorePlayer.class;
}
@Override
public String getDefaultLabelPrefix(MemBuffer buf, Settings settings, int len,
DataTypeDisplayOptions options) {
return "MIDI";
}
@Override
public DataType getReplacementBaseType() {
return ByteDataType.dataType;
}
}

View file

@ -0,0 +1,91 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.program.model.data;
import java.awt.event.MouseEvent;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import javax.sound.midi.*;
import javax.swing.Icon;
import generic.theme.GIcon;
import ghidra.util.Msg;
import ghidra.util.Swing;
/**
* Plays a MIDI score
*/
public class ScorePlayer implements Playable, MetaEventListener {
private static final Icon MIDI_ICON = new GIcon("icon.data.type.audio.player");
private static final int END_OF_TRACK_MESSAGE = 47;
// This currently only allows one sequence to be played for the entire application,
// which seems good enough. The MIDI instance variables are currently synchronized
// by the Swing thread.
private static volatile Sequencer currentSequencer;
private byte[] bytes;
public ScorePlayer(byte[] bytes) {
this.bytes = bytes;
}
@Override
public Icon getImageIcon() {
return MIDI_ICON;
}
@Override
public void clicked(MouseEvent event) {
try {
// Any new request should stop any previous sequence being played
if (currentSequencer != null) {
stop();
return;
}
Sequencer sequencer = MidiSystem.getSequencer(true);
sequencer.addMetaEventListener(this);
sequencer.setLoopCount(0);
sequencer.setSequence(MidiSystem.getSequence(new ByteArrayInputStream(bytes)));
sequencer.open();
currentSequencer = sequencer;
currentSequencer.start();
}
catch (MidiUnavailableException | InvalidMidiDataException | IOException e) {
Msg.error(this, "Unable to play score", e);
}
}
@Override
public void meta(MetaMessage message) {
if (message.getType() == END_OF_TRACK_MESSAGE) {
Swing.runNow(() -> stop());
}
}
private void stop() {
if (currentSequencer == null) {
return;
}
currentSequencer.removeMetaEventListener(this);
currentSequencer.stop();
currentSequencer.close();
currentSequencer = null;
}
}