3131import java .io .InputStream ;
3232import java .nio .charset .StandardCharsets ;
3333import java .util .Arrays ;
34+ import java .util .stream .Collectors ;
35+ import java .util .stream .IntStream ;
3436
3537import static javax .sound .midi .ShortMessage .*;
3638import static net .raphimc .noteblocklib .format .midi .MidiDefinitions .*;
@@ -49,24 +51,25 @@ public static MidiSong parseSong(final Sequence sequence, final String fileName)
4951 return parseSong (sequence , fileName , true );
5052 }
5153
52- public static MidiSong parseSong (final Sequence sequence , final String fileName , final boolean skipOutOfRangeNotes ) {
53- final MidiSong song = new MidiSong (fileName );
54-
54+ public static MidiSong parseSong (final Sequence sequence , final String fileName , final boolean skipOutOfNbsRangeNotes ) {
5555 if (sequence .getTickLength () > Integer .MAX_VALUE ) {
5656 throw new IllegalArgumentException ("MIDI sequence has too many ticks" );
5757 }
5858
59+ final MidiSong song = new MidiSong (fileName );
5960 if (sequence .getDivisionType () == Sequence .PPQ ) {
60- song .getTempoEvents ().set (0 , (float ) (1_000_000D / ((double ) MidiDefinitions . DEFAULT_TEMPO_MPQ / sequence .getResolution ())));
61+ song .getTempoEvents ().set (0 , (float ) (1_000_000D / ((double ) DEFAULT_TEMPO_MPQ / sequence .getResolution ())));
6162 } else {
6263 song .getTempoEvents ().set (0 , sequence .getResolution () * sequence .getDivisionType ());
6364 }
6465
65- final byte [] channelInstruments = new byte [MidiDefinitions .CHANNEL_COUNT ];
66- final byte [] channelVolumes = new byte [MidiDefinitions .CHANNEL_COUNT ];
67- final byte [] channelPans = new byte [MidiDefinitions .CHANNEL_COUNT ];
68- Arrays .fill (channelVolumes , MidiDefinitions .MAX_VELOCITY );
69- Arrays .fill (channelPans , MidiDefinitions .CENTER_PAN );
66+ final byte [] channelInstruments = new byte [CHANNEL_COUNT ];
67+ final byte [] channelVolumes = new byte [CHANNEL_COUNT ];
68+ final byte [] channelPans = new byte [CHANNEL_COUNT ];
69+ final byte [] channelExpressions = new byte [CHANNEL_COUNT ];
70+ Arrays .fill (channelVolumes , DEFAULT_VOLUME );
71+ Arrays .fill (channelPans , CENTER_PAN );
72+ Arrays .fill (channelExpressions , Byte .MAX_VALUE );
7073
7174 for (int trackIdx = 0 ; trackIdx < sequence .getTracks ().length ; trackIdx ++) {
7275 final Track track = sequence .getTracks ()[trackIdx ];
@@ -78,82 +81,169 @@ public static MidiSong parseSong(final Sequence sequence, final String fileName,
7881 final ShortMessage shortMessage = (ShortMessage ) message ;
7982 switch (shortMessage .getCommand ()) {
8083 case NOTE_ON :
84+ final byte key = (byte ) MathUtil .clamp (shortMessage .getData1 (), LOWEST_KEY , HIGHEST_KEY );
85+ final byte velocity = (byte ) MathUtil .clamp (shortMessage .getData2 (), 0 , MAX_VELOCITY );
8186 final byte instrument = channelInstruments [shortMessage .getChannel ()];
82- final byte key = (byte ) shortMessage .getData1 ();
83- final byte velocity = (byte ) shortMessage .getData2 ();
87+ final byte volume = channelVolumes [shortMessage .getChannel ()];
8488 final byte pan = channelPans [shortMessage .getChannel ()];
89+ final byte expression = channelExpressions [shortMessage .getChannel ()];
8590
8691 final Note note = new Note ();
8792 if (shortMessage .getChannel () == PERCUSSION_CHANNEL ) {
8893 final PercussionMapping mapping = MidiMappings .PERCUSSION_MAPPINGS [key ];
89- if (mapping == null ) continue ;
94+ if (mapping == null ) {
95+ continue ;
96+ }
9097
9198 note .setInstrument (mapping .getInstrument ());
9299 note .setNbsKey (mapping .getNbsKey ());
93100 } else {
94101 final InstrumentMapping mapping = MidiMappings .INSTRUMENT_MAPPINGS [instrument ];
95- if (mapping == null ) continue ;
102+ if (mapping == null ) {
103+ continue ;
104+ }
96105
97106 note .setInstrument (mapping .getInstrument ());
98107 note .setMidiKey (MathUtil .clamp (key + KEYS_PER_OCTAVE * mapping .getOctaveModifier (), LOWEST_KEY , HIGHEST_KEY ));
99108 }
100- note .setVolume (((float ) velocity / MAX_VELOCITY ) * (float ) channelVolumes [shortMessage .getChannel ()] / MAX_VELOCITY );
101- note .setPanning ((float ) (pan - CENTER_PAN ) / CENTER_PAN );
102-
103- if (skipOutOfRangeNotes && (note .getMidiKey () < NbsDefinitions .LOWEST_MIDI_KEY || note .getMidiKey () > NbsDefinitions .HIGHEST_MIDI_KEY )) {
109+ if (skipOutOfNbsRangeNotes && (note .getMidiKey () < NbsDefinitions .LOWEST_MIDI_KEY || note .getMidiKey () > NbsDefinitions .HIGHEST_MIDI_KEY )) {
104110 continue ;
105111 }
106-
112+ note .setVolume (((float ) velocity / MAX_VELOCITY ) * ((float ) volume / MAX_VELOCITY ) * ((float ) expression / MAX_VELOCITY ));
113+ if (pan < CENTER_PAN ) { // 0-63 (64 values) -> left
114+ note .setPanning ((float ) (pan - CENTER_PAN ) / CENTER_PAN );
115+ } else if (pan > CENTER_PAN ) { // 65-127 (63 values) -> right
116+ note .setPanning ((float ) (pan - CENTER_PAN ) / (Byte .MAX_VALUE - CENTER_PAN ));
117+ }
107118 song .getNotes ().add ((int ) event .getTick (), note );
108119 break ;
109120 case NOTE_OFF :
110121 // Ignore note off events
111122 break ;
112123 case PROGRAM_CHANGE :
113- channelInstruments [shortMessage .getChannel ()] = (byte ) shortMessage .getData1 ();
124+ channelInstruments [shortMessage .getChannel ()] = (byte ) Math . max (( byte ) shortMessage .getData1 (), 0 );
114125 break ;
115126 case CONTROL_CHANGE :
116127 switch (shortMessage .getData1 ()) {
117- case VOLUME_CONTROL_MSB :
118- channelVolumes [shortMessage .getChannel ()] = (byte ) shortMessage .getData2 ();
128+ case CONTROL_CHANNEL_VOLUME_MSB :
129+ channelVolumes [shortMessage .getChannel ()] = (byte ) MathUtil . clamp ( shortMessage .getData2 (), 0 , MAX_VELOCITY );
119130 break ;
120- case PAN_CONTROL_MSB :
121- channelPans [shortMessage .getChannel ()] = (byte ) shortMessage .getData2 ();
131+ case CONTROL_PAN_MSB :
132+ channelPans [shortMessage .getChannel ()] = (byte ) MathUtil . clamp ( shortMessage .getData2 (), 0 , Byte . MAX_VALUE );
122133 break ;
123- case RESET_CONTROLS :
124- channelVolumes [shortMessage .getChannel ()] = MAX_VELOCITY ;
125- channelPans [shortMessage .getChannel ()] = CENTER_PAN ;
134+ case CONTROL_EXPRESSION_CONTROLLER_MSB :
135+ channelExpressions [shortMessage .getChannel ()] = (byte ) MathUtil .clamp (shortMessage .getData2 (), 0 , Byte .MAX_VALUE );
136+ break ;
137+ case CONTROL_RESET_ALL_CONTROLLERS :
138+ // Most MIDI synths don't reset volume and pan
139+ channelExpressions [shortMessage .getChannel ()] = Byte .MAX_VALUE ;
126140 break ;
127141 }
128142 break ;
129143 case PITCH_BEND :
130144 // Ignore pitch bend events
131145 break ;
132- case SYSTEM_RESET :
133- Arrays .fill (channelInstruments , (byte ) 0 );
134- Arrays .fill (channelVolumes , MAX_VELOCITY );
135- Arrays .fill (channelPans , CENTER_PAN );
146+ case CHANNEL_PRESSURE :
147+ // Ignore channel pressure events
148+ break ;
149+ case POLY_PRESSURE :
150+ // Ignore poly pressure events
136151 break ;
152+ default :
153+ throw new IllegalStateException ("Unsupported MIDI command: " + shortMessage .getCommand ());
137154 }
138155 } else if (message instanceof MetaMessage ) {
139156 final MetaMessage metaMessage = (MetaMessage ) message ;
140157 final byte [] data = metaMessage .getData ();
141- if (metaMessage .getType () == META_SET_TEMPO && data .length == 3 && sequence .getDivisionType () == Sequence .PPQ ) {
142- final int newMpq = ((data [0 ] & 0xFF ) << 16 ) | ((data [1 ] & 0xFF ) << 8 ) | (data [2 ] & 0xFF );
143- final double microsPerTick = (double ) newMpq / sequence .getResolution ();
144- song .getTempoEvents ().set ((int ) event .getTick (), (float ) (1_000_000D / microsPerTick ));
145- } else if (metaMessage .getType () == META_COPYRIGHT_NOTICE ) {
146- song .setOriginalAuthor (new String (data , StandardCharsets .US_ASCII ));
147- } else if (metaMessage .getType () == META_TRACK_NAME ) {
148- song .getTrackNames ().put (trackIdx , new String (data , StandardCharsets .US_ASCII ));
158+ switch (metaMessage .getType ()) {
159+ case META_SET_TEMPO :
160+ if (data .length == 3 && sequence .getDivisionType () == Sequence .PPQ ) {
161+ final int newMpq = ((data [0 ] & 0xFF ) << 16 ) | ((data [1 ] & 0xFF ) << 8 ) | (data [2 ] & 0xFF );
162+ final double microsPerTick = (double ) newMpq / sequence .getResolution ();
163+ song .getTempoEvents ().set ((int ) event .getTick (), (float ) (1_000_000D / microsPerTick ));
164+ }
165+ break ;
166+ case META_TEXT :
167+ final String text = Arrays .stream (new String (data , StandardCharsets .US_ASCII ).split ("\n " ))
168+ .map (String ::trim )
169+ .filter (line -> !line .isEmpty ())
170+ .map (line -> "Text: " + line )
171+ .collect (Collectors .joining ("\n " ));
172+ if (!text .isEmpty ()) {
173+ if (song .getDescription () == null ) {
174+ song .setDescription (text );
175+ } else {
176+ song .setDescription (song .getDescription () + "\n " + text );
177+ }
178+ }
179+ break ;
180+ case META_COPYRIGHT_NOTICE :
181+ final String copyright = Arrays .stream (new String (data , StandardCharsets .US_ASCII ).split ("\n " ))
182+ .map (String ::trim )
183+ .filter (line -> !line .isEmpty ())
184+ .map (line -> "Copyright: " + line )
185+ .collect (Collectors .joining ("\n " ));
186+ if (!copyright .isEmpty ()) {
187+ if (song .getDescription () == null ) {
188+ song .setDescription (copyright );
189+ } else {
190+ song .setDescription (song .getDescription () + "\n " + copyright );
191+ }
192+ }
193+ break ;
194+ case META_TRACK_NAME :
195+ final String trackName = Arrays .stream (new String (data , StandardCharsets .US_ASCII ).split ("\n " ))
196+ .map (String ::trim )
197+ .filter (line -> !line .isEmpty ())
198+ .map (line -> "Track Name: " + line )
199+ .collect (Collectors .joining ("\n " ));
200+ if (!trackName .isEmpty ()) {
201+ if (song .getDescription () == null ) {
202+ song .setDescription (trackName );
203+ } else {
204+ song .setDescription (song .getDescription () + "\n " + trackName );
205+ }
206+ }
207+ break ;
149208 }
209+ } else if (message instanceof SysexMessage ) {
210+ final SysexMessage sysexMessage = (SysexMessage ) message ;
211+ if (sysexMessage .getStatus () == SysexMessage .SYSTEM_EXCLUSIVE ) {
212+ final byte [] data = sysexMessage .getData ();
213+ if (data .length == 4 && (data [0 ] & 0xFF ) == SYSEX_UNIVERSAL_NON_REALTIME_MESSAGE && (data [1 ] & 0xFF ) == SYSEX_DEVICE_ALL && (data [2 ] & 0xFF ) == SYSEX_SUB_ID_GENERAL_MIDI ) {
214+ final int subId2 = data [3 ] & 0xFF ;
215+ if (subId2 == SYSEX_GENERAL_MIDI_GM1_SYSTEM_ON || subId2 == SYSEX_GENERAL_MIDI_GM2_SYSTEM_ON ) {
216+ Arrays .fill (channelInstruments , (byte ) 0 );
217+ Arrays .fill (channelVolumes , DEFAULT_VOLUME );
218+ Arrays .fill (channelPans , CENTER_PAN );
219+ Arrays .fill (channelExpressions , Byte .MAX_VALUE );
220+ }
221+ }
222+ }
223+ } else {
224+ throw new IllegalStateException ("Unsupported MIDI message type: " + message .getClass ().getName ());
150225 }
151226 }
152227 }
153228
154- final float maxTempo = song .getTempoEvents ().getTempoRange ()[1 ];
155- if (maxTempo > SONG_TARGET_TEMPO ) {
156- SongResampler .changeTickSpeed (song , SONG_TARGET_TEMPO );
229+ if (song .getTempoEvents ().getTempoRange ()[1 ] > SONG_TARGET_TEMPO ) {
230+ final double [] times = SongResampler .getNotesByTime (song ).keySet ().stream ().mapToDouble (Double ::doubleValue ).sorted ().toArray ();
231+ final double [] timeSpaces = IntStream .range (1 , times .length ).mapToDouble (i -> times [i ] - times [i - 1 ]).sorted ().toArray ();
232+ if (timeSpaces .length > 0 ) {
233+ final float minTimeSpace = (float ) timeSpaces [0 ];
234+ final float p05TimeSpace = (float ) timeSpaces [timeSpaces .length / 20 ];
235+ final float p10TimeSpace = (float ) timeSpaces [timeSpaces .length / 10 ];
236+ final float [] candidateTempos = new float []{1000F / minTimeSpace , 1000F / p05TimeSpace , 1000F / p10TimeSpace };
237+ for (float candidateTempo : candidateTempos ) {
238+ if (candidateTempo <= SONG_TARGET_TEMPO ) {
239+ SongResampler .changeTickSpeed (song , candidateTempo );
240+ break ;
241+ }
242+ }
243+ }
244+ if (song .getTempoEvents ().getTempoRange ()[1 ] > SONG_TARGET_TEMPO ) {
245+ SongResampler .changeTickSpeed (song , SONG_TARGET_TEMPO );
246+ }
157247 }
158248
159249 return song ;
0 commit comments