1 module pixelperfectengine.audio.base.midiseq; 2 3 import mididi; 4 import midi2.types.enums; 5 import midi2.types.structs; 6 import core.time : Duration, usecs; 7 8 import pixelperfectengine.audio.base.modulebase; 9 10 /** 11 * Intended to synchronize any sequencer with audio playback. 12 */ 13 public interface Sequencer { 14 /** 15 * Makes the sequencer to go forward by the given amount of time, and emits MIDI commands to the associated modules 16 * if the time have reached that point. 17 * Params: 18 * amount = the time amount that have been lapsed, ideally the buffer length in time, alternatively a frame delta 19 * if one wants to tie MIDI sequencing to screen update rate (not recommended). 20 */ 21 public void lapseTime(Duration amount) @nogc nothrow; 22 } 23 /** 24 * Implements a MIDI v1.0 sequencer. 25 * 26 * Since MIDI v2.0 isn't widespread (and seems like I even have to implement my own format) and v1.0 is widespread and 27 * still capable enough (even if getting the most out of it needs some klunkiness), I'm creating an internal sequencer 28 * for this format too. 29 * By using multiple tracks, it's able to interface with multiple modules. 30 */ 31 public class SequencerM1 : Sequencer { 32 enum Status { 33 IsRunning = 1<<0, ///Set if sequencer is running 34 LoopEnable = 1<<1, ///Set if looping is enabled 35 FoundLoop = 1<<2, ///Set if sequencer found a loop point 36 Ended = 1<<8, ///Set if the sequence ended naturally 37 } 38 protected uint status; ///Stores status flags, see enum Status 39 protected MIDI src; ///Source file deconstructed. 40 protected AudioModule[] modules; ///Module list. 41 public uint[] routing; ///Routings for multi-track MIDI files. 42 public ubyte[] routGrp; ///Group routing for each module. 43 protected Duration[] positionTime; ///Current time position for all individual tracks. 44 protected size_t[] positionBlock; ///Current event position for all individual tracks. 45 protected uint[] usecPerTic; ///Precalculated microseconds per tic (per track) for less complexity. 46 ///States of the tracks. 47 ///Bit 0 : End of track marker has reached. 48 protected ubyte[] trackState; 49 ///Save states for looping. 50 protected Duration[] lp_positionTime; 51 protected size_t[] lp_positionBlock; 52 protected uint[] lp_usecPerTic; 53 protected ubyte[] lp_trackState; 54 public this(AudioModule[] modules, uint[] routing, ubyte[] routGrp) @safe pure nothrow { 55 this.modules = modules; 56 this.routing = routing; 57 this.routGrp = routGrp; 58 } 59 /** 60 * Loads a MIDI file into the sequencer, and initializes some basic data. 61 * Params: 62 * src = the MIDI file to be loaded. 63 */ 64 public void openMIDI(MIDI src) @safe { 65 this.src = src; 66 //positionTime.length = 0; 67 positionTime.length = src.headerChunk.nTracks; 68 //positionBlock.length = 0; 69 positionBlock.length = src.headerChunk.nTracks; 70 //usecPerTic.length = 0; 71 usecPerTic.length = src.headerChunk.nTracks; 72 //trackState.length = 0; 73 trackState.length = src.headerChunk.nTracks; 74 } 75 /** 76 * Starts the sequencer. 77 */ 78 public void start() @nogc @safe pure nothrow { 79 status |= Status.IsRunning; 80 } 81 /** 82 * Stops the sequencer. 83 */ 84 public void stop() @nogc @safe pure nothrow { 85 status &= ~Status.IsRunning; 86 reset(); 87 } 88 /** 89 * Resets the sequencer. 90 */ 91 public void reset() @nogc @safe pure nothrow { 92 for (ushort i ; i < usecPerTic.length ; i++) { 93 setTimeDiv(500_000,i); ///Assume 120 beats per second. 94 positionTime[i] = Duration.init; 95 positionBlock[i] = size_t.init; 96 trackState[i] = ubyte.init; 97 } 98 } 99 /** 100 * Pauses the sequencer. 101 * Note: Won't pause states of associated modules. 102 */ 103 public void pause() @nogc @safe pure nothrow { 104 status &= ~Status.IsRunning; 105 } 106 /** 107 * Enables looping (marked with `LOOPBEGIN` and `LOOPEND`), then repeates the MIDI data between these points until 108 * either looping gets disabled, or the sequencer gets shut down. 109 * Params: 110 * val = 111 */ 112 public bool enableLoop(bool val) @nogc @safe pure nothrow { 113 if (val) 114 status |= Status.LoopEnable; 115 else 116 status &= ~Status.LoopEnable; 117 return (status & Status.LoopEnable) != 0; 118 } 119 /** 120 * Sets the time division for the given track 121 * Params: 122 * usecPerQNote = Microseconds per quarter note, in case if a tempo change event happens. 123 * track = The track number, in case if a tempo change event happens. 124 */ 125 protected final void setTimeDiv(uint usecPerQNote, size_t track = 0) @nogc @safe pure nothrow { 126 if (src.headerChunk.division.getFormat == 0) { 127 usecPerTic[track] = usecPerQNote / src.headerChunk.division.getTicksPerQuarterNote(); 128 } else { 129 switch (src.headerChunk.division.getNegativeSMPTEFormat()) { 130 case -29: 131 usecPerTic[track] = cast(uint)(29.97 * src.headerChunk.division.getTicksPerFrame()); 132 break; 133 default: 134 usecPerTic[track] = cast(uint)(-1 * src.headerChunk.division.getNegativeSMPTEFormat() * 135 src.headerChunk.division.getTicksPerFrame()); 136 break; 137 } 138 } 139 } 140 /** 141 * Makes the sequencer to go forward by the given amount of time, and emits MIDI commands to the associated modules 142 * if the time have reached that point. 143 * Params: 144 * amount = the time amount that have been lapsed, ideally the buffer length in time, alternatively a frame delta 145 * if one wants to tie MIDI sequencing to screen update rate. 146 */ 147 public void lapseTime(Duration amount) @nogc nothrow { 148 if (!(status & Status.IsRunning)) return; 149 foreach (size_t i , ref Duration d ; positionTime) { 150 if (!(trackState[i] & 1) && (positionBlock[i] < src.trackChunks[i].events.length)) { 151 d += amount; 152 Duration toEvent = ticsToDuration(src.trackChunks[i].events[positionBlock[i]].deltaTime, i); 153 if (d >= toEvent) { //process event 154 d = toEvent - d; 155 //MIDIEvent currEv = src.trackChunks[i].events[positionBlock[i]]; 156 switch (src.trackChunks[i].events[positionBlock[i]].statusByte()) { 157 case 0xF0: ///SYSEX event 158 SysExEvent* ev = src.trackChunks[i].events[positionBlock[i]].asSysExEvent; 159 if (ev.data.length <= 6) { 160 UMP first = UMP(MessageType.Data64, routGrp[i], SysExSt.Complete, cast(ubyte)ev.data.length); 161 uint second; 162 if (ev.data.length > 1) 163 first.bytes[2] = ev.data[0]; 164 if (ev.data.length > 2) 165 first.bytes[3] = ev.data[1]; 166 for (int j = 2 ; i < ev.data.length ; j++) { 167 second |= ev.data[j]<<(24 - (8 * (2-j))); 168 } 169 modules[routing[i]].midiReceive(first, second); 170 } else { 171 size_t pos; 172 while (pos < ev.data.length) { 173 const sizediff_t diff = ev.data.length - pos; 174 ubyte sysExSt = SysExSt.Cont; 175 if (!diff) sysExSt = SysExSt.Start; 176 else if (diff < 6) sysExSt = SysExSt.End; 177 UMP first = UMP(MessageType.Data64, routGrp[i], sysExSt, cast(ubyte)ev.data.length); 178 uint second; 179 if (diff > 1) 180 first.bytes[2] = ev.data[pos + 0]; 181 if (diff > 2) 182 first.bytes[3] = ev.data[pos + 1]; 183 for (int j = 2 ; i < diff && j < 6 ; j++) { 184 second |= ev.data[pos + j]<<(24 - (8 * (2-j))); 185 } 186 pos += 6; 187 modules[routing[i]].midiReceive(first, second); 188 } 189 } 190 break; 191 case 0xFF: ///Meta event 192 MetaEvent* ev = src.trackChunks[i].events[positionBlock[i]].asMetaEvent; 193 switch (ev.type) { 194 case MetaEventType.setTempo: 195 setTimeDiv((ev.data[0]<<16) | (ev.data[1]<<8) | ev.data[2], i); 196 break; 197 case MetaEventType.endOfTrack: 198 trackState[i] |= 1; 199 break; 200 case MetaEventType.marker, MetaEventType.cuePoint: 201 switch (cast(string)ev.data) { //Process looppoint events 202 case "LOOPBEGIN"://Save states 203 lp_positionBlock = positionBlock; 204 lp_positionTime = positionTime; 205 lp_trackState = trackState; 206 lp_usecPerTic = usecPerTic; 207 status |= Status.FoundLoop; 208 break; 209 case "LOOPEND": 210 if ((status & Status.LoopEnable) && (status & Status.FoundLoop)) { 211 positionBlock = lp_positionBlock; 212 positionTime = lp_positionTime; 213 trackState = lp_trackState; 214 usecPerTic = lp_usecPerTic; 215 } 216 break; 217 default: 218 break; 219 } 220 break; 221 default: 222 break; 223 } 224 break; 225 default: ///MIDI event 226 MIDIEvent* ev = src.trackChunks[i].events[positionBlock[i]].asMIDIEvent; 227 modules[routing[i]].midiReceive(UMP(MessageType.MIDI1, routGrp[i], ev.statusByte>>4, ev.statusByte & 0x0F, 228 ev.data[0], ev.data[1])); 229 break; 230 } 231 positionBlock[i]++; 232 } 233 } 234 } 235 } 236 /** 237 * Converts MIDI tics to Duration. 238 * Params: 239 * tics = The MIDI tics. 240 * track = The MIDI track itself. 241 * Returns: The 242 */ 243 final protected Duration ticsToDuration(int tics, size_t track) @nogc @safe pure nothrow const { 244 return usecs(tics * usecPerTic[track]); 245 } 246 }