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.trackFormat == TrackFormat.simultaneous) { 127 if (src.headerChunk.division.getFormat == 0) { 128 for (int t ; t < usecPerTic.length ; t++) 129 usecPerTic[t] = (usecPerQNote / src.headerChunk.division.getTicksPerQuarterNote()) * 10; 130 } else { 131 switch (src.headerChunk.division.getNegativeSMPTEFormat()) { 132 case -29: 133 for (int t ; t < usecPerTic.length ; t++) 134 usecPerTic[t] = cast(uint)(29.97 * src.headerChunk.division.getTicksPerFrame()); 135 break; 136 default: 137 for (int t ; t < usecPerTic.length ; t++) 138 usecPerTic[t] = cast(uint)(-1 * src.headerChunk.division.getNegativeSMPTEFormat() * 139 src.headerChunk.division.getTicksPerFrame()); 140 break; 141 } 142 } 143 } else { 144 if (src.headerChunk.division.getFormat == 0) { 145 usecPerTic[track] = (usecPerQNote / src.headerChunk.division.getTicksPerQuarterNote()) * 10; 146 } else { 147 switch (src.headerChunk.division.getNegativeSMPTEFormat()) { 148 case -29: 149 usecPerTic[track] = cast(uint)(29.97 * src.headerChunk.division.getTicksPerFrame()); 150 break; 151 default: 152 usecPerTic[track] = cast(uint)(-1 * src.headerChunk.division.getNegativeSMPTEFormat() * 153 src.headerChunk.division.getTicksPerFrame()); 154 break; 155 } 156 } 157 } 158 } 159 /** 160 * Makes the sequencer to go forward by the given amount of time, and emits MIDI commands to the associated modules 161 * if the time have reached that point. 162 * Params: 163 * amount = the time amount that have been lapsed, ideally the buffer length in time, alternatively a frame delta 164 * if one wants to tie MIDI sequencing to screen update rate. 165 */ 166 public void lapseTime(Duration amount) @nogc nothrow { 167 if (!(status & Status.IsRunning)) return; 168 foreach (size_t i , ref Duration d ; positionTime) { //process each channel 169 d += amount; //shift position by current time delta 170 while (d > usecs(0) && (positionBlock[i] < src.trackChunks[i].events.length) && !(trackState[i] & 1)) { 171 Duration toEvent = ticsToDuration(src.trackChunks[i].events[positionBlock[i]].deltaTime, i); 172 if (d >= toEvent) { 173 d -= toEvent; 174 switch (src.trackChunks[i].events[positionBlock[i]].statusByte()) { 175 case 0xF0: ///SYSEX event 176 SysExEvent* ev = src.trackChunks[i].events[positionBlock[i]].asSysExEvent; 177 if (ev.data.length <= 6) { 178 UMP first = UMP(MessageType.Data64, routGrp[i], SysExSt.Complete, cast(ubyte)ev.data.length); 179 uint second; 180 if (ev.data.length > 1) 181 first.bytes[2] = ev.data[0]; 182 if (ev.data.length > 2) 183 first.bytes[3] = ev.data[1]; 184 for (int j = 2 ; i < ev.data.length ; j++) { 185 second |= ev.data[j]<<(24 - (8 * (2-j))); 186 } 187 modules[routing[i]].midiReceive(first, second); 188 } else { 189 size_t pos; 190 while (pos < ev.data.length) { 191 const sizediff_t diff = ev.data.length - pos; 192 ubyte sysExSt = SysExSt.Cont; 193 if (!diff) sysExSt = SysExSt.Start; 194 else if (diff < 6) sysExSt = SysExSt.End; 195 UMP first = UMP(MessageType.Data64, routGrp[i], sysExSt, cast(ubyte)ev.data.length); 196 uint second; 197 if (diff > 1) 198 first.bytes[2] = ev.data[pos + 0]; 199 if (diff > 2) 200 first.bytes[3] = ev.data[pos + 1]; 201 for (int j = 2 ; i < diff && j < 6 ; j++) { 202 second |= ev.data[pos + j]<<(24 - (8 * (2-j))); 203 } 204 pos += 6; 205 modules[routing[i]].midiReceive(first, second); 206 } 207 } 208 break; 209 case 0xFF: ///Meta event 210 MetaEvent* ev = src.trackChunks[i].events[positionBlock[i]].asMetaEvent; 211 switch (ev.type) { 212 case MetaEventType.setTempo: 213 setTimeDiv((ev.data[0]<<16) | (ev.data[1]<<8) | ev.data[2], i); 214 break; 215 case MetaEventType.endOfTrack: 216 trackState[i] |= 1; 217 break; 218 case MetaEventType.marker, MetaEventType.cuePoint: 219 switch (cast(string)ev.data) { //Process looppoint events 220 case "LOOPBEGIN"://Save states 221 lp_positionBlock = positionBlock; 222 lp_positionTime = positionTime; 223 lp_trackState = trackState; 224 lp_usecPerTic = usecPerTic; 225 status |= Status.FoundLoop; 226 break; 227 case "LOOPEND": 228 if ((status & Status.LoopEnable) && (status & Status.FoundLoop)) { 229 positionBlock = lp_positionBlock; 230 positionTime = lp_positionTime; 231 trackState = lp_trackState; 232 usecPerTic = lp_usecPerTic; 233 } 234 break; 235 default: 236 break; 237 } 238 break; 239 default: 240 break; 241 } 242 break; 243 default: ///MIDI event 244 MIDIEvent* ev = src.trackChunks[i].events[positionBlock[i]].asMIDIEvent; 245 modules[routing[i]].midiReceive(UMP(MessageType.MIDI1, routGrp[i], ev.statusByte>>4, ev.statusByte & 0x0F, 246 ev.data[0], ev.data[1])); 247 break; 248 } 249 positionBlock[i]++; 250 } else { 251 break; 252 } 253 } 254 } 255 } 256 /** 257 * Converts MIDI tics to Duration. 258 * Params: 259 * tics = The MIDI tics. 260 * track = The MIDI track itself. 261 * Returns: The 262 */ 263 final protected Duration ticsToDuration(int tics, size_t track) @nogc @safe pure nothrow const { 264 return usecs(tics * usecPerTic[track]); 265 } 266 }