diff --git a/Ghidra/Features/Base/ghidra_scripts/FindAudioInProgramScript.java b/Ghidra/Features/Base/ghidra_scripts/FindAudioInProgramScript.java index bc49371a36..3b04838e24 100644 --- a/Ghidra/Features/Base/ghidra_scripts/FindAudioInProgramScript.java +++ b/Ghidra/Features/Base/ghidra_scripts/FindAudioInProgramScript.java @@ -15,17 +15,16 @@ */ //Finds programs containing various audio resources such as WAV's //@category Resources +import java.util.ArrayList; +import java.util.List; + import ghidra.app.script.GhidraScript; import ghidra.program.model.address.Address; -import ghidra.program.model.data.DataType; -import ghidra.program.model.data.WAVEDataType; +import ghidra.program.model.data.*; import ghidra.program.model.listing.Data; import ghidra.program.model.mem.Memory; import ghidra.program.model.mem.MemoryBlock; -import java.util.ArrayList; -import java.util.List; - public class FindAudioInProgramScript extends GhidraScript { @Override @@ -36,8 +35,10 @@ public class FindAudioInProgramScript extends GhidraScript { //look for WAV data types WAVEDataType wdt = new WAVEDataType(); + MIDIDataType mdt = new MIDIDataType(); totalFound += findAudioData("WAV", wdt, WAVEDataType.MAGIC, WAVEDataType.MAGIC_MASK); + totalFound += findAudioData("MIDI", mdt, MIDIDataType.MAGIC, MIDIDataType.MAGIC_MASK); if (totalFound == 0) { println("No Audio data found in " + currentProgram.getName()); @@ -54,12 +55,12 @@ public class FindAudioInProgramScript extends GhidraScript { int numDataFound = 0; List
foundList = scanForAudioData(pattern, mask); - //Loop over all potential found WAVs + //Loop over all potential found audio for (int i = 0; i < foundList.size(); i++) { boolean foundData = false; - //See if already applied WAV + //See if already applied data type Data data = getDataAt(foundList.get(i)); - //If not already applied, try to apply WAV data type + //If not already applied, try to apply audio data type if (data == null) { println("Trying to apply " + dataName + " datatype at " + foundList.get(i).toString()); @@ -67,7 +68,7 @@ public class FindAudioInProgramScript extends GhidraScript { try { Data newData = createData(foundList.get(i), dt); if (newData != null) { - println("Applied WAV at " + newData.getAddressString(false, true)); + printf("Applied %s at %s", dataName, newData.getAddressString(false, true)); foundData = true; } } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/EmbeddedMediaAnalyzer.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/EmbeddedMediaAnalyzer.java index 141404a300..334acc3a1f 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/EmbeddedMediaAnalyzer.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/EmbeddedMediaAnalyzer.java @@ -81,6 +81,9 @@ public class EmbeddedMediaAnalyzer extends AbstractAnalyzer { addByteSearchPattern(searcher, program, foundMedia, new WAVEDataType(), "WAVE", WAVEDataType.MAGIC, WAVEDataType.MAGIC_MASK); + addByteSearchPattern(searcher, program, foundMedia, new MIDIDataType(), "MIDI", + MIDIDataType.MAGIC, MIDIDataType.MAGIC_MASK); + addByteSearchPattern(searcher, program, foundMedia, new AUDataType(), "AU", AUDataType.MAGIC, AUDataType.MAGIC_MASK); diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/pe/ResourceDataDirectory.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/pe/ResourceDataDirectory.java index b6aa8db00b..688211a313 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/pe/ResourceDataDirectory.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/pe/ResourceDataDirectory.java @@ -269,6 +269,19 @@ public class ResourceDataDirectory extends DataDirectory { } PeUtils.createData(program, addr, dataType, log); } + else if (info.getName().startsWith("Rsrc_MIDI")) { + DataType dataType = null; + // Check for MIDI magic number + try { + if (program.getMemory().getInt(addr) == 0x6468544d) { + dataType = new MIDIDataType(); + } + } + catch (MemoryAccessException e) { + // ignore - let createData produce error + } + PeUtils.createData(program, addr, dataType, log); + } else if (info.getName().startsWith("Rsrc_WEVT")) { DataType dataType = null; // Check for WEVT magic number "CRIM" diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/AudioPlayer.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/AudioPlayer.java index ab1c2a8241..7f8146753f 100644 --- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/AudioPlayer.java +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/AudioPlayer.java @@ -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); } } diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/MIDIDataType.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/MIDIDataType.java new file mode 100644 index 0000000000..4b2ab9eb6a --- /dev/null +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/MIDIDataType.java @@ -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 ""; + } + + @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; + } + +} diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/ScorePlayer.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/ScorePlayer.java new file mode 100644 index 0000000000..b9dfa674b9 --- /dev/null +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/ScorePlayer.java @@ -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; + } +}