1 module pixelperfectengine.audio.m2.seq; 2 3 public import pixelperfectengine.audio.m2.types; 4 public import pixelperfectengine.audio.base.midiseq : Sequencer; 5 public import pixelperfectengine.audio.base.modulebase; 6 import collections.treemap; 7 8 import std.typecons : BitFlags; 9 import core.time : MonoTime; 10 import midi2.types.structs; 11 import midi2.types.enums; 12 13 public class SequencerM2 : Sequencer { 14 enum ErrorFlags : uint { 15 badOpcode = 1<<0, 16 illegalJump = 1<<1, 17 unrecognizedDevice= 1<<2, 18 unrecognizedCode= 1<<3, 19 outOfPatternSlots= 1<<4, 20 unrecognizedPattern= 1<<5, 21 illegalMIDICmd = 1<<6, 22 } 23 enum StatusFlags : uint { 24 play = 1<<0, 25 pause = 1<<1, 26 endReached = 1<<8, 27 28 cfg_StopOnError = 1<<16, 29 } 30 public BitFlags!ErrorFlags errors; 31 protected BitFlags!StatusFlags status; 32 protected Duration timePos; 33 public TreeMap!(uint, AudioModule) modTrgt; 34 public M2Song songdata; 35 36 public void lapseTime(Duration amount) @nogc nothrow { 37 if (!status.play) return; 38 timePos += amount; 39 foreach (ref M2PatternSlot ptrnSl ; songdata.ptrnSl) { 40 if (ptrnSl.status.isRunning && !ptrnSl.status.suspend && status.play) { 41 advancePattern(ptrnSl, amount); 42 } 43 } 44 } 45 /** 46 * Advances the supplied pattern by the given `amount`, then processes commands if needed. 47 */ 48 private void advancePattern(ref M2PatternSlot ptrn, Duration amount) @nogc nothrow { 49 ///Returns the current timebase 50 ulong getTimeBase() @nogc @safe pure nothrow const { 51 return (((songdata.timebase * songdata.globTimeMult)>>16) * ptrn.timeMult)>>16; 52 } 53 if ((ptrn.timeToWait -= amount) <= hnsecs(0)) { //If enough time has passed, then proceed to compute commands 54 uint[] patternData = songdata.ptrnData[ptrn.id]; 55 while (patternData.length > ptrn.position && status.play) { //Loop until end is reached or certain opcode is reached (handle that within the switch statement) 56 DataReaderHelper data = DataReaderHelper(patternData[ptrn.position]); 57 switch (data.bytes[0]) { //Process commands 58 case OpCode.nullcmd: //Null command (do nothing) 59 break; 60 case OpCode.lnwait: //Long wait 61 const ulong tics = (((data.bytes[1]<<16) | data.hwords[1])<<24) | patternData[ptrn.position + 1]; //Get amount of tics for this wait command 62 const ulong timeBase = getTimeBase(); 63 ptrn.timeToWait += nsecs(timeBase * tics); //calculate new wait amount, plus amount for any inaccuracy from sequencer steping. 64 ptrn.position += 2; 65 if (!ptrn.timeToWait.isNegative) goto exitLoop; //hazard case: even after wait time is l 66 break; 67 case OpCode.shwait: //Short wait 68 const uint tics = (data.bytes[1]<<16) | data.hwords[1]; 69 const ulong timeBase = getTimeBase(); 70 ptrn.timeToWait += nsecs(timeBase * tics); 71 ptrn.position += 1; 72 if (!ptrn.timeToWait.isNegative) goto exitLoop; 73 break; 74 case OpCode.emit: //MIDI data emit 75 const device = data.hwords[1]; 76 const dataAm = data.bytes[1]; 77 emitMIDIData(patternData[ptrn.position..ptrn.position + dataAm], device); 78 ptrn.position += 1 + dataAm; 79 break; 80 case OpCode.jmp: //Conditional jump 81 switch (data.bytes[1]) { 82 case JmpCode.nc: //Always jump 83 ptrn.position += cast(int)patternData[ptrn.position + 2]; 84 break; 85 case JmpCode.eq: //Jump if condition code and condition register are equal 86 if (ptrn.localReg[CR] == patternData[ptrn.position + 1]) { 87 ptrn.position += cast(int)patternData[ptrn.position + 2]; 88 } else { 89 ptrn.position += 3; 90 } 91 break; 92 case JmpCode.ne: //Jump if condition code and condition register are not equal 93 if (ptrn.localReg[CR] != patternData[ptrn.position + 1]) { 94 ptrn.position += cast(int)patternData[ptrn.position + 2]; 95 } else { 96 ptrn.position += 3; 97 } 98 break; 99 case JmpCode.sh: 100 if (ptrn.localReg[CR] & patternData[ptrn.position + 1]) { 101 ptrn.position += cast(int)patternData[ptrn.position + 2]; 102 } else { 103 ptrn.position += 3; 104 } 105 break; 106 case JmpCode.op: //Jump if condition code bits are opposite of CR 107 if (ptrn.localReg[CR] == ~patternData[ptrn.position + 1]) { 108 ptrn.position += cast(int)patternData[ptrn.position + 2]; 109 } else { 110 ptrn.position += 3; 111 } 112 break; 113 default: 114 ptrn.position += 3; 115 errors.unrecognizedCode = true; 116 if (status.cfg_StopOnError) { 117 status.play = false; 118 return; 119 } 120 break; 121 } 122 if (ptrn.position > patternData.length) { 123 errors.illegalJump = true; 124 ptrn.status.isRunning = false; 125 ptrn.status.hasEnded = true; 126 if (status.cfg_StopOnError) { 127 status.play = false; 128 return; 129 } 130 } 131 break; 132 case OpCode.chain_par: 133 initNewPattern((data.bytes[1]<<24) | data.hwords[1], PATTERN_SLOT_INACTIVE_ID); 134 ptrn.position++; 135 break; 136 case OpCode.chain_ser: 137 initNewPattern((data.bytes[1]<<24) | data.hwords[1], ptrn.id); 138 ptrn.status.suspend = true; 139 ptrn.position++; 140 return; 141 //Math operations on registers begin 142 case OpCode.add: .. case OpCode.satmuls: 143 T saturate(T)(const long src) @nogc nothrow pure { 144 if (src < T.min) return T.min; 145 if (src > T.max) return T.max; 146 return cast(T)src; 147 } 148 const uint ra = data.bytes[1] & 0x80 ? songdata.globalReg[data.bytes[1]&0x7F] : ptrn.localReg[data.bytes[1]]; 149 const uint rb = data.bytes[2] & 0x80 ? songdata.globalReg[data.bytes[2]&0x7F] : ptrn.localReg[data.bytes[2]]; 150 uint rd; 151 switch (data.bytes[0]) { 152 case OpCode.add: 153 rd = ra + rb; 154 break; 155 case OpCode.sub: 156 rd = ra - rb; 157 break; 158 case OpCode.mul: 159 rd = ra * rb; 160 break; 161 case OpCode.div: 162 rd = ra / rb; 163 break; 164 case OpCode.mod: 165 rd = ra % rb; 166 break; 167 case OpCode.and: 168 rd = ra & rb; 169 break; 170 case OpCode.or: 171 rd = ra | rb; 172 break; 173 case OpCode.xor: 174 rd = ra ^ rb; 175 break; 176 case OpCode.not: 177 rd = ~ra; 178 break; 179 case OpCode.lshi: 180 rd = ra << data.bytes[2]; 181 break; 182 case OpCode.rshi: 183 rd = ra >> data.bytes[2]; 184 break; 185 case OpCode.rasi: 186 rd = cast(int)ra >> data.bytes[2]; 187 break; 188 case OpCode.adds: 189 rd = cast(int)ra + cast(int)rb; 190 break; 191 case OpCode.subs: 192 rd = cast(int)ra - cast(int)rb; 193 break; 194 case OpCode.muls: 195 rd = cast(int)ra * cast(int)rb; 196 break; 197 case OpCode.divs: 198 rd = cast(int)ra / cast(int)rb; 199 break; 200 case OpCode.lsh: 201 rd = ra << rb; 202 break; 203 case OpCode.rsh: 204 rd = ra << rb; 205 break; 206 case OpCode.ras: 207 rd = cast(int)ra << cast(int)rb; 208 break; 209 case OpCode.mov: 210 rd = ra; 211 break; 212 case OpCode.satadd: 213 rd = saturate!uint(cast(long)ra + rb); 214 break; 215 case OpCode.satsub: 216 rd = saturate!uint(cast(long)ra - rb); 217 break; 218 case OpCode.satmul: 219 rd = saturate!uint(cast(long)ra * rb); 220 break; 221 case OpCode.satadds: 222 rd = saturate!int(cast(long)(cast(int)ra) + cast(int)rb); 223 break; 224 case OpCode.satsubs: 225 rd = saturate!int(cast(long)(cast(int)ra) - cast(int)rb); 226 break; 227 case OpCode.satmuls: 228 rd = saturate!int(cast(long)(cast(int)ra) * cast(int)rb); 229 break; 230 default: 231 errors.badOpcode = true; 232 if (status.cfg_StopOnError) { 233 status.play = false; 234 return; 235 } 236 break; 237 } 238 if (data.bytes[3] & 0x80) { 239 songdata.globalReg[data.bytes[3] & 0x7F] = rd; 240 } else { 241 ptrn.localReg[data.bytes[3]] = rd; 242 } 243 ptrn.position++; 244 break; 245 //Math operations on registers end 246 case OpCode.cmp: //compare instruction 247 bool cmpRes; 248 const uint ra = data.bytes[2] & 0x80 ? songdata.globalReg[data.bytes[2]&0x7F] : ptrn.localReg[data.bytes[2]]; 249 const uint rb = data.bytes[3] & 0x80 ? songdata.globalReg[data.bytes[3]&0x7F] : ptrn.localReg[data.bytes[3]]; 250 switch (data.bytes[1]) { 251 case CmpCode.eq: 252 cmpRes = ra == rb; 253 break; 254 case CmpCode.ne: 255 cmpRes = ra != rb; 256 break; 257 case CmpCode.gt: 258 cmpRes = ra > rb; 259 break; 260 case CmpCode.ge: 261 cmpRes = ra >= rb; 262 break; 263 case CmpCode.lt: 264 cmpRes = ra < rb; 265 break; 266 case CmpCode.le: 267 cmpRes = ra <= rb; 268 break; 269 case CmpCode.ze: 270 cmpRes = ra == 0; 271 break; 272 case CmpCode.nz: 273 cmpRes = ra != 0; 274 break; 275 case CmpCode.ng: 276 cmpRes = cast(int)ra < 0; 277 break; 278 case CmpCode.po: 279 cmpRes = cast(int)ra > 0; 280 break; 281 case CmpCode.sgt: 282 cmpRes = cast(int)ra > cast(int)rb; 283 break; 284 case CmpCode.sge: 285 cmpRes = cast(int)ra >= cast(int)rb; 286 break; 287 case CmpCode.slt: 288 cmpRes = cast(int)ra < cast(int)rb; 289 break; 290 case CmpCode.sle: 291 cmpRes = cast(int)ra <= cast(int)rb; 292 break; 293 default: 294 errors.unrecognizedCode = true; 295 if (status.cfg_StopOnError) { 296 status.play = false; 297 return; 298 } 299 break; 300 } 301 ptrn.localReg[CR]<<=1; //Shift in new bit depending on compare result; 302 if (cmpRes) ptrn.localReg[CR] |= 1; 303 ptrn.position++; 304 break; 305 case OpCode.chain: 306 ptrn.status.isRunning = false; 307 ptrn.status.hasEnded = true; 308 initNewPattern((data.bytes[1]<<24) | data.hwords[1], PATTERN_SLOT_INACTIVE_ID); 309 return; 310 case OpCode.cue: 311 ptrn.lastCue = (data.bytes[1]<<24) | data.hwords[1]; 312 break; 313 default: 314 errors.badOpcode = true; 315 if (status.cfg_StopOnError) { 316 status.play = false; 317 return; 318 } 319 break; 320 } 321 } 322 exitLoop: 323 if (patternData.length >= ptrn.position && !hasUsefulDataLeft(patternData[ptrn.position..$])) { //Free up pattern slot if ended or has no useful data left. 324 ptrn.status.isRunning = false; 325 ptrn.status.hasEnded = true; 326 if (ptrn.backLink != PATTERN_SLOT_INACTIVE_ID) { 327 foreach (size_t i, ref M2PatternSlot ptrnSl ; songdata.ptrnSl) { 328 if (ptrnSl.id == ptrn.backLink) { 329 ptrn.status.suspend = false; 330 } 331 } 332 } 333 } 334 } 335 } 336 ///Initializes new pattern with the given ID 337 private void initNewPattern(uint patternID, uint backLink) @nogc nothrow { 338 foreach (size_t i, ref M2PatternSlot ptrnSl ; songdata.ptrnSl) { 339 if (ptrnSl.id == PATTERN_SLOT_INACTIVE_ID || ptrnSl.status.hasEnded) { //Search for lowest unused pattern slot 340 ptrnSl.reset; 341 uint[] ptrnData = songdata.ptrnData[patternID]; 342 if (ptrnData.length) { 343 ptrnSl.id = patternID; 344 ptrnSl.backLink = backLink; 345 } else { //Handle errors for potential references to nonexistent patterns 346 errors.unrecognizedPattern = true; 347 if (status.cfg_StopOnError) { 348 status.play = false; 349 } 350 } 351 return; 352 } 353 } 354 errors.outOfPatternSlots = true; 355 if (status.cfg_StopOnError) { 356 status.play = false; 357 } 358 } 359 ///Returns true if there's still useful for the sequencer. 360 private bool hasUsefulDataLeft(uint[] patternData) @nogc nothrow pure const { 361 if (patternData.length == 0) return false; 362 foreach (key; patternData) { 363 DataReaderHelper data = DataReaderHelper(key); 364 if (data.bytes[0] != 0x00 || data.bytes[0] != 0xff) return true; //Has at least one potential command. 365 } 366 return false; 367 } 368 /** 369 * Emits MIDI data to the target module. Takes care of data length, transposing (TODO: implement), etc. 370 */ 371 private void emitMIDIData(uint[] data, uint targetID) @nogc nothrow { 372 uint pos; 373 AudioModule am = modTrgt[targetID]; 374 if (am is null) { 375 errors.unrecognizedCode = true; 376 if (status.cfg_StopOnError) status.play = false; 377 return; 378 } 379 while (pos < data.length) { 380 UMP midiPck; 381 midiPck.base = data[pos]; 382 const uint cmdSize = umpSizes[midiPck.msgType]>>4; 383 if (cmdSize + pos >= data.length) { 384 errors.illegalMIDICmd = true; 385 if (status.cfg_StopOnError) status.play = false; 386 return; 387 } else { 388 switch (cmdSize) { 389 case 2: 390 am.midiReceive(midiPck, data[pos + 1]); 391 pos += 2; 392 break; 393 case 3: 394 am.midiReceive(midiPck, data[pos + 1], data[pos + 2]); 395 pos += 3; 396 break; 397 case 4: 398 am.midiReceive(midiPck, data[pos + 1], data[pos + 2], data[pos + 3]); 399 pos += 4; 400 break; 401 default: 402 am.midiReceive(midiPck); 403 pos += 1; 404 break; 405 } 406 } 407 } 408 } 409 }