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 }