From a10c00911ae1b59b87157cee333b679bc78f26cd Mon Sep 17 00:00:00 2001 From: Alessandro Gatti Date: Wed, 20 Mar 2024 18:27:34 +0100 Subject: [PATCH 1/3] Add embedded MIDI data type with its media player. --- .../program/model/data/MIDIDataType.java | 173 ++++++++++++++++++ .../program/model/data/ScorePlayer.java | 101 ++++++++++ 2 files changed, 274 insertions(+) create mode 100644 Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/MIDIDataType.java create mode 100644 Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/ScorePlayer.java 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..4a93231918 --- /dev/null +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/MIDIDataType.java @@ -0,0 +1,173 @@ +/* ### + * 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.EOFException; +import java.io.IOException; +import java.io.InputStream; + +import ghidra.docking.settings.Settings; +import ghidra.program.model.mem.MemBuffer; +import ghidra.program.model.mem.Memory; +import ghidra.util.Msg; + +public class MIDIDataType extends BuiltIn implements Dynamic { + 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 { + return computeLength(buf, maxLength); + } + catch (Exception e) { + Msg.debug(this, "Invalid MIDI data at " + buf.getAddress()); + } + return -1; + } + + private long readUnsignedInteger(InputStream stream) throws IOException { + long value = 0; + for (int index = 0; index < 4; index++) { + int currentByte = stream.read(); + if (currentByte == -1) { + throw new EOFException(); + } + value = (value << 8) | currentByte; + } + return value; + } + + private int readUnsignedShort(InputStream stream) throws IOException { + int value = 0; + for (int index = 0; index < 2; index++) { + int currentByte = stream.read(); + if (currentByte == -1) { + throw new EOFException(); + } + value = (value << 8) | currentByte; + } + return value; + } + + private int computeLength(MemBuffer buf, int maxLength) throws IOException, InvalidDataTypeException { + int computedLength = -1; + + try (InputStream stream = 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 = readUnsignedInteger(stream); + if (chunkLength != 6) { + throw new InvalidDataTypeException("Unexpected header length."); + } + stream.skip(2); + int tracks = readUnsignedShort(stream); + stream.skip(2); + computedLength = 14; + while (tracks > 0) { + if (stream.read(chunkType) < chunkType.length) { + throw new EOFException(); + } + chunkLength = readUnsignedInteger(stream); + 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--; + } + } finally { + } + + return computedLength; + } + + @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..d8c7dd33c4 --- /dev/null +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/ScorePlayer.java @@ -0,0 +1,101 @@ +/* ### + * 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.InvalidMidiDataException; +import javax.sound.midi.MetaEventListener; +import javax.sound.midi.MetaMessage; +import javax.sound.midi.MidiSystem; +import javax.sound.midi.MidiUnavailableException; +import javax.sound.midi.Sequence; +import javax.sound.midi.Sequencer; +import javax.swing.Icon; + +import generic.theme.GIcon; +import ghidra.util.Msg; +import ghidra.util.Swing; + +public class ScorePlayer implements Playable, MetaEventListener { + + private static final int END_OF_TRACK_MESSAGE = 47; + + private static final Icon AUDIO_ICON = new GIcon("icon.data.type.audio.player"); + + // 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 Sequence currentSequence; + private static volatile Sequencer currentSequencer; + + private byte[] bytes; + + public ScorePlayer(byte[] bytes) { + this.bytes = bytes; + } + + @Override + public Icon getImageIcon() { + return AUDIO_ICON; + } + + @Override + public void clicked(MouseEvent event) { + try { + // any new request should stop any previous sequence being played + if (currentSequence != null && currentSequencer != null) { + assert currentSequencer.isOpen(); + currentSequencer.stop(); + currentSequence = null; // this field is also updated when the sound thread calls back + currentSequencer = null; // same as above + return; + } + + Sequencer sequencer = MidiSystem.getSequencer(true); + sequencer.addMetaEventListener(this); + sequencer.setLoopCount(0); + Sequence sequence = MidiSystem.getSequence(new ByteArrayInputStream(bytes)); + if (!sequencer.isOpen()) { + sequencer.open(); + } + sequencer.setSequence(sequence); + currentSequence = sequence; + currentSequencer = sequencer; + currentSequencer.start(); + } + catch (MidiUnavailableException | InvalidMidiDataException | IOException e) { + Msg.debug(this, "Unable to play score", e); + } + } + + @Override + public void meta(MetaMessage message) { + if (message.getType() != END_OF_TRACK_MESSAGE) { + return; + } + + assert currentSequencer != null && currentSequence != null; + currentSequencer.removeMetaEventListener(this); + currentSequencer.stop(); + Swing.runNow(() -> { + currentSequence = null; + currentSequencer = null; + }); + } +} From 30600863cf12d8a4db42007f44ae8fd2e3d7928b Mon Sep 17 00:00:00 2001 From: Ryan Kurtz Date: Thu, 2 May 2024 12:41:22 -0400 Subject: [PATCH 2/3] GP-4516: Improvements --- .../FindAudioInProgramScript.java | 19 ++-- .../core/analysis/EmbeddedMediaAnalyzer.java | 3 + .../bin/format/pe/ResourceDataDirectory.java | 13 +++ .../program/model/data/MIDIDataType.java | 86 +++++++------------ .../program/model/data/ScorePlayer.java | 50 +++++------ 5 files changed, 75 insertions(+), 96 deletions(-) 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/MIDIDataType.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/MIDIDataType.java index 4a93231918..4b2ab9eb6a 100644 --- 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 @@ -4,9 +4,9 @@ * 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. @@ -15,16 +15,25 @@ */ package ghidra.program.model.data; +import java.io.DataInputStream; import java.io.EOFException; -import java.io.IOException; -import java.io.InputStream; import ghidra.docking.settings.Settings; import ghidra.program.model.mem.MemBuffer; -import ghidra.program.model.mem.Memory; 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); } @@ -40,80 +49,43 @@ public class MIDIDataType extends BuiltIn implements Dynamic { @Override public int getLength(MemBuffer buf, int maxLength) { - try { - return computeLength(buf, maxLength); - } - catch (Exception e) { - Msg.debug(this, "Invalid MIDI data at " + buf.getAddress()); - } - return -1; - } - - private long readUnsignedInteger(InputStream stream) throws IOException { - long value = 0; - for (int index = 0; index < 4; index++) { - int currentByte = stream.read(); - if (currentByte == -1) { - throw new EOFException(); - } - value = (value << 8) | currentByte; - } - return value; - } - - private int readUnsignedShort(InputStream stream) throws IOException { - int value = 0; - for (int index = 0; index < 2; index++) { - int currentByte = stream.read(); - if (currentByte == -1) { - throw new EOFException(); - } - value = (value << 8) | currentByte; - } - return value; - } - - private int computeLength(MemBuffer buf, int maxLength) throws IOException, InvalidDataTypeException { - int computedLength = -1; - - try (InputStream stream = buf.getInputStream(0, maxLength > 0 ? maxLength : Integer.MAX_VALUE)) { + 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') { + if (chunkType[0] != (byte) 'M' || chunkType[1] != (byte) 'T' || + chunkType[2] != (byte) 'h' || chunkType[3] != (byte) 'd') { return -1; } - long chunkLength = readUnsignedInteger(stream); + long chunkLength = Integer.toUnsignedLong(stream.readInt()); if (chunkLength != 6) { throw new InvalidDataTypeException("Unexpected header length."); } stream.skip(2); - int tracks = readUnsignedShort(stream); + int tracks = Short.toUnsignedInt(stream.readShort()); stream.skip(2); - computedLength = 14; + int computedLength = 14; while (tracks > 0) { if (stream.read(chunkType) < chunkType.length) { throw new EOFException(); } - chunkLength = readUnsignedInteger(stream); + 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') { + if (chunkType[0] != (byte) 'M' || chunkType[1] != (byte) 'T' || + chunkType[2] != (byte) 'r' || chunkType[3] != (byte) 'k') { continue; } tracks--; } - } finally { + return computedLength; + } + catch (Exception e) { + Msg.debug(this, "Invalid MIDI data at " + buf.getAddress()); + return -1; } - - return computedLength; } @Override 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 index d8c7dd33c4..48251df628 100644 --- 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 @@ -19,29 +19,24 @@ import java.awt.event.MouseEvent; import java.io.ByteArrayInputStream; import java.io.IOException; -import javax.sound.midi.InvalidMidiDataException; -import javax.sound.midi.MetaEventListener; -import javax.sound.midi.MetaMessage; -import javax.sound.midi.MidiSystem; -import javax.sound.midi.MidiUnavailableException; -import javax.sound.midi.Sequence; -import javax.sound.midi.Sequencer; +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; - private static final Icon AUDIO_ICON = new GIcon("icon.data.type.audio.player"); - // 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 Sequence currentSequence; private static volatile Sequencer currentSequencer; private byte[] bytes; @@ -52,30 +47,23 @@ public class ScorePlayer implements Playable, MetaEventListener { @Override public Icon getImageIcon() { - return AUDIO_ICON; + return MIDI_ICON; } @Override public void clicked(MouseEvent event) { try { - // any new request should stop any previous sequence being played - if (currentSequence != null && currentSequencer != null) { - assert currentSequencer.isOpen(); - currentSequencer.stop(); - currentSequence = null; // this field is also updated when the sound thread calls back - currentSequencer = null; // same as above + // 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); - Sequence sequence = MidiSystem.getSequence(new ByteArrayInputStream(bytes)); - if (!sequencer.isOpen()) { - sequencer.open(); - } - sequencer.setSequence(sequence); - currentSequence = sequence; + sequencer.setSequence(MidiSystem.getSequence(new ByteArrayInputStream(bytes))); + sequencer.open(); currentSequencer = sequencer; currentSequencer.start(); } @@ -86,16 +74,18 @@ public class ScorePlayer implements Playable, MetaEventListener { @Override public void meta(MetaMessage message) { - if (message.getType() != END_OF_TRACK_MESSAGE) { + if (message.getType() == END_OF_TRACK_MESSAGE) { + Swing.runNow(() -> stop()); + } + } + + private void stop() { + if (currentSequencer == null) { return; } - - assert currentSequencer != null && currentSequence != null; currentSequencer.removeMetaEventListener(this); currentSequencer.stop(); - Swing.runNow(() -> { - currentSequence = null; - currentSequencer = null; - }); + currentSequencer.close(); + currentSequencer = null; } } From 7c37564d967a81544812bcae1fbbcdeacb18ddaf Mon Sep 17 00:00:00 2001 From: Ryan Kurtz Date: Tue, 7 May 2024 06:24:18 -0400 Subject: [PATCH 3/3] GP-4516: Review --- .../src/main/java/ghidra/program/model/data/AudioPlayer.java | 2 +- .../src/main/java/ghidra/program/model/data/ScorePlayer.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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/ScorePlayer.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/ScorePlayer.java index 48251df628..b9dfa674b9 100644 --- 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 @@ -68,7 +68,7 @@ public class ScorePlayer implements Playable, MetaEventListener { currentSequencer.start(); } catch (MidiUnavailableException | InvalidMidiDataException | IOException e) { - Msg.debug(this, "Unable to play score", e); + Msg.error(this, "Unable to play score", e); } }