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 }