SmfTrack GenerateTrack(MmlResolvedTrack source) { var rtrk = new SmfTrack (); int cur = 0; foreach (var ev in source.Events) { SmfEvent evt; if (ev.Arguments.Count == 3) evt = new SmfEvent (ev.Arguments [0], ev.Arguments [1], ev.Arguments [2], null); else if (ev.Arguments [0] == 0xFF) evt = new SmfEvent (ev.Arguments [0], ev.Arguments [1], 0, ev.Arguments.Skip (2).ToArray ()); else evt = new SmfEvent (ev.Arguments [0], 0, 0, ev.Arguments.Skip (1).ToArray ()); var msg = new SmfMessage (ev.Tick - cur, evt); rtrk.Messages.Add (msg); cur = ev.Tick; } rtrk.Messages.Add (new SmfMessage (0, new SmfEvent (0xFF, 0x2F, 0, new byte [0]))); return rtrk; }
// extraTailArgs is a list of arguments that are passed to the context macro call e.g. // #macro CHORD_A c0e0g // 1 CHORD_A4 CHORD_A8 CHORD_A8 // In this case, 4, 8 and 8 are NOT passed to CHORD_A unless these extraTailArgs are passed // and causes unexpected outputs. // We can still define macros to take full arguments to the defined sequence of operators // (in this case, to 'g'), but that is super annoying and basically impossible unless // you know the macro definition details (which almost no one would know). void ProcessOperations(MmlResolvedTrack track, MmlResolveContext rctx, List<MmlOperationUse> list, int start, int count, IEnumerable<MmlValueExpr> extraTailArgs) { int storeIndex = -1; List<MmlResolvedEvent> storeCurrentOutput = null, storeDummy = new List<MmlResolvedEvent> (); StoredOperations currentStoredOperations = null; bool is_string_format = false; for (int listIndex = start; listIndex < start + count; listIndex++) { var oper = list [listIndex]; var extraTailArgsIfApplied = listIndex == start + count - 1 ? extraTailArgs : null; switch (oper.Name) { case "__PRINT": { oper.Arguments [0].Resolve (rctx, MmlDataType.String); DebugPrint.WriteLine (oper.Arguments [0].StringValue); break; } case "__LET": { oper.Arguments [0].Resolve (rctx, MmlDataType.String); string name = oper.Arguments [0].StringValue; var variable = (MmlSemanticVariable) source.Variables [name]; if (variable == null) throw new MmlException (String.Format ("Target variable not found: {0}", name), location); oper.Arguments [1].Resolve (rctx, variable.Type); rctx.Values [variable] = oper.Arguments [1].ResolvedValue; if (name == "__timeline_position") rctx.TimelinePosition = oper.Arguments [1].IntValue; break; } case "__STORE": { oper.Arguments [0].Resolve (rctx, MmlDataType.String); oper.ValidateArguments (rctx, oper.Arguments.Count); string name = oper.Arguments [0].StringValue; var variable = (MmlSemanticVariable) source.Variables [name]; if (variable == null) throw new MmlException (String.Format ("Target variable not found: {0}", name), location); if (variable.Type != MmlDataType.Buffer) throw new MmlException (String.Format ("Target variable is not a buffer: {0}", name), location); var sb = (StringBuilder) rctx.EnsureDefaultResolvedVariable (variable); for (int i = 1; i < oper.Arguments.Count; i++) sb.Append (oper.Arguments [i].StringValue); break; } case "__FORMAT": is_string_format = true; goto case "__STORE_FORMAT"; case "__STORE_FORMAT": { oper.Arguments [0].Resolve (rctx, MmlDataType.String); oper.Arguments [1].Resolve (rctx, MmlDataType.String); oper.ValidateArguments (rctx, oper.Arguments.Count); string name = oper.Arguments [0].StringValue; string format = oper.Arguments [1].StringValue; var variable = (MmlSemanticVariable) source.Variables [name]; if (variable == null) throw new MmlException (String.Format ("Target variable not found: {0}", name), location); if (is_string_format) { if (variable.Type != MmlDataType.String) throw new MmlException (String.Format ("Target variable is not a string: {0}", name), location); } else { if (variable.Type != MmlDataType.Buffer) throw new MmlException (String.Format ("Target variable is not a buffer: {0}", name), location); } try { string v = string.Format (format, (object []) (from x in oper.Arguments.Skip (2) select (object) x.StringValue).ToArray ()); if (is_string_format) rctx.Values [variable] = v; else ((StringBuilder) rctx.EnsureDefaultResolvedVariable (variable)).Append (v); } catch (FormatException ex) { throw new MmlException (String.Format ("Format error while applying '{0}' to '{1}': {2}", format, name, ex.Message), location); } break; } case "__APPLY": var oa = oper.Arguments [0]; oa.Resolve (rctx, MmlDataType.String); string apparg = oa.StringValue; // add macro argument definitions var tmpop = new MmlOperationUse (apparg, oper.Location); for (int x = 1; x < oper.Arguments.Count; x++) tmpop.Arguments.Add (oper.Arguments [x]); ProcessMacroCall (track, rctx, tmpop, extraTailArgsIfApplied); break; case "__MIDI": oper.ValidateArguments (rctx, oper.Arguments.Count); var mop = new MmlResolvedEvent ("MIDI", rctx.TimelinePosition); foreach (var arg in oper.Arguments) mop.Arguments.Add (arg.ByteValue); current_output.Add (mop); if (recordNextAsChord) chord.Add (mop); recordNextAsChord = false; break; case "__SYNC_NOFF_WITH_NEXT": recordNextAsChord = true; break; case "__ON_MIDI_NOTE_OFF": // handle zero-length note oper.ValidateArguments (rctx, 3, MmlDataType.Number, MmlDataType.Number, MmlDataType.Number); if (oper.Arguments [0].IntValue == 0) // record next note as part of chord recordNextAsChord = true; else { foreach (MmlResolvedEvent cop in chord) cop.Tick += oper.Arguments [0].IntValue; chord.Clear (); } break; case "__MIDI_META": oper.ValidateArguments (rctx, oper.Arguments.Count); var mmop = new MmlResolvedEvent ("META", rctx.TimelinePosition); mmop.Arguments.Add (0xFF); foreach (var arg in oper.Arguments) mmop.Arguments.AddRange (arg.ByteArrayValue); current_output.Add (mmop); break; case "__SAVE_OPER_BEGIN": oper.ValidateArguments (rctx, 0); if (storeIndex >= 0) throw new MmlException ("__SAVE_OPER_BEGIN works only within a simple list", oper.Location); storeIndex = listIndex + 1; storeCurrentOutput = current_output; current_output = storeDummy; currentStoredOperations = new StoredOperations (); currentStoredOperations.Values = new Dictionary<MmlSemanticVariable, object> (rctx.Values); currentStoredOperations.MacroArguments = (Hashtable) rctx.MacroArguments.Clone (); break; case "__SAVE_OPER_END": { oper.ValidateArguments (rctx, 1, MmlDataType.Number); int bufIdx = oper.Arguments [0].IntValue; stored_operations [bufIdx] = currentStoredOperations; currentStoredOperations.Operations = new List<MmlOperationUse> (list.Skip (storeIndex).Take (listIndex - storeIndex - 1)); current_output = storeCurrentOutput; storeDummy.Clear (); storeIndex = -1; currentStoredOperations = null; // FIXME: might be better to restore variables break; } case "__RESTORE_OPER": { oper.ValidateArguments (rctx, 1, MmlDataType.Number); int bufIdx = oper.Arguments [0].IntValue; var ss = stored_operations [bufIdx]; var valuesBak = rctx.Values; var macroArgsBak = rctx.MacroArguments; rctx.Values = ss.Values; rctx.MacroArguments = ss.MacroArguments; // adjust timeline_position (no need to update rctx.TimelinePosition here). rctx.Values [(MmlSemanticVariable) source.Variables ["__timeline_position"]] = rctx.TimelinePosition; ProcessOperations (track, rctx, ss.Operations, 0, ss.Operations.Count, extraTailArgsIfApplied); rctx.Values = valuesBak; rctx.MacroArguments = macroArgsBak; break; } case "__LOOP_BEGIN": #if !UNHACK_LOOP case "[": #endif oper.ValidateArguments (rctx, 0); var loop = new Loop (rctx) { BeginAt= new LoopLocation (listIndex, current_output.Count, rctx.TimelinePosition) }; rctx.Values = new Dictionary<MmlSemanticVariable,object> (loop.SavedValues.Count); foreach (var p in loop.SavedValues) rctx.Values.Add (p.Key, p.Value); rctx.Loops.Push (loop); current_output = loop.Events; break; case "__LOOP_BREAK": #if !UNHACK_LOOP case "/": case ":": #endif oper.ValidateArguments (rctx, oper.Arguments.Count); loop = rctx.CurrentLoop; if (loop == null) throw new MmlException ("Loop break operation must be inside a pair of loop start and end", location); if (loop.FirstBreakAt == null) loop.FirstBreakAt = new LoopLocation (listIndex, current_output.Count, rctx.TimelinePosition); foreach (var cl in loop.CurrentBreaks) loop.EndLocations [cl] = new LoopLocation (listIndex, current_output.Count, rctx.TimelinePosition); loop.CurrentBreaks.Clear (); // FIXME: actually this logic does not make sense as now it is defined with fixed-length arguments... if (oper.Arguments.Count == 0) { // default loop break if (loop.Breaks.ContainsKey (-1) && loop.Breaks.Values.All (b => b.Source != listIndex)) throw new MmlException ("Default loop break is already defined in current loop", location); loop.Breaks.Add (-1, new LoopLocation (listIndex, current_output.Count, rctx.TimelinePosition)); loop.CurrentBreaks.Add (-1); } else { for (int x = 0; x < oper.Arguments.Count; x++) { var numexpr = oper.Arguments [x]; var num = numexpr.IntValue - 1; // "1st. loop" for musicians == 0th iteration in code. if (x > 0 && num < 0) break; // after the last argument. loop.CurrentBreaks.Add (num); if (loop.Breaks.ContainsKey (num) && loop.Breaks.Values.All (b => b.Source != listIndex)) throw new MmlException (String.Format ("Loop section {0} was already defined in current loop", num), location); // specified loop count is for human users. Here the number is for program, hence -1. loop.Breaks.Add (num, new LoopLocation (listIndex, current_output.Count, rctx.TimelinePosition)); } } break; case "__LOOP_END": #if !UNHACK_LOOP case "]": #endif oper.ValidateArguments (rctx, 0, MmlDataType.Number); loop = rctx.CurrentLoop; if (loop == null) throw new MmlException ("Loop has not started", location); foreach (var cl in loop.CurrentBreaks) loop.EndLocations [cl] = new LoopLocation (listIndex, current_output.Count, rctx.TimelinePosition); int loopCount; switch (oper.Arguments.Count) { case 0: loopCount = 2; break; case 1: loopCount = oper.Arguments [0].IntValue; break; default: throw new MmlException ("Arguments at loop end exceeded", location); } rctx.Loops.Pop (); var outside = rctx.CurrentLoop; current_output = outside != null ? outside.Events : track.Events; // now expand loop. // - verify that every loop break does not exceed the loop count // - add sequence before the first break // - add sequence for each break. If no explicit break, then use default. foreach (var p in loop.Breaks) { if (p.Key > loopCount) throw new MmlException ("Loop break specified beyond the loop count", list [p.Value.Source].Location); } rctx.Values = loop.SavedValues; int baseTicks; int baseOutputEnd; int tickOffset = 0; if (loop.FirstBreakAt == null) { // w/o break baseTicks = rctx.TimelinePosition - loop.BeginAt.Tick; baseOutputEnd = loop.Events.Count; rctx.TimelinePosition = loop.BeginAt.Tick; // This range of commands actually adds extra argument definitions for loop operation, but it won't hurt. for (int l = 0; l < loopCount; l++) ProcessOperations (track, rctx, list, loop.BeginAt.Source + 1, listIndex - loop.BeginAt.Source - 1, extraTailArgsIfApplied); } else { // w/ breaks baseTicks = loop.FirstBreakAt.Tick - loop.BeginAt.Tick; baseOutputEnd = loop.FirstBreakAt.Output; rctx.TimelinePosition = loop.BeginAt.Tick; for (int l = 0; l < loopCount; l++) { ProcessOperations (track, rctx, list, loop.BeginAt.Source + 1, loop.FirstBreakAt.Source - loop.BeginAt.Source - 1, extraTailArgsIfApplied); tickOffset += baseTicks; LoopLocation lb = null; if (!loop.Breaks.TryGetValue (l, out lb)) { if (l + 1 == loopCount) break; // this is to break the loop at the last iteration. if (!loop.Breaks.TryGetValue (-1, out lb)) throw new MmlException (String.Format ("No corresponding loop break specification for iteration at {0} from the innermost loop", l + 1), list [loop.BeginAt.Source].Location); } if (lb == null) // final break break; LoopLocation elb; if (!loop.EndLocations.TryGetValue (l, out elb)) elb = loop.EndLocations [-1]; int breakOffset = lb.Tick - loop.BeginAt.Tick + baseTicks; ProcessOperations (track, rctx, list, lb.Source + 1, elb.Source - lb.Source - 1, extraTailArgsIfApplied); } } break; default: ProcessMacroCall (track, rctx, oper, extraTailArgsIfApplied); break; } } }
void ProcessMacroCall(MmlResolvedTrack track, MmlResolveContext ctx, MmlOperationUse oper, IEnumerable<MmlValueExpr> extraTailArgs) { var macro = (MmlSemanticMacro) track.Macros [oper.Name]; if (macro == null) throw new MmlException (String.Format ("Macro {0} was not found. {1}", oper.Name, oper.Location), location); if (expansion_stack.Contains (macro)) throw new MmlException (String.Format ("Illegally recursive macro reference to {0} is found", macro.Name), null); expansion_stack.Push (macro); if (cache_stack_num == arg_caches.Count) arg_caches.Add (new Hashtable ()); var args = arg_caches [cache_stack_num++]; var operUseArgs = extraTailArgs != null ? oper.Arguments.Concat (extraTailArgs).ToList () : oper.Arguments; for (int i = 0; i < macro.Arguments.Count; i++) { MmlSemanticVariable argdef = macro.Arguments [i]; MmlValueExpr arg = i < operUseArgs.Count ? operUseArgs [i] : null; if (arg == null) arg = argdef.DefaultValue; arg.Resolve (ctx, argdef.Type); if (args.Contains (argdef.Name)) throw new MmlException (String.Format ("Argument name must be identical to all other argument names. Argument '{0}' in '{1}' macro", argdef.Name, oper.Name), oper.Location); args.Add (argdef.Name, new KeyValuePair<MmlSemanticVariable, object> (argdef, arg.ResolvedValue)); } var argsBak = ctx.MacroArguments; ctx.MacroArguments = args; var extraTailArgsToCall = macro.Arguments.Count < operUseArgs.Count ? operUseArgs.Skip (macro.Arguments.Count) : null; ProcessOperations (track, ctx, macro.Data, 0, macro.Data.Count, extraTailArgsToCall); ctx.MacroArguments = argsBak; expansion_stack.Pop (); args.Clear (); --cache_stack_num; }
void Generate() { global_context = new MmlResolveContext (source, null); foreach (var track in source.Tracks) { var rtrk = new MmlResolvedTrack (track.Number, source); result.Tracks.Add (rtrk); var tctx = new MmlResolveContext (source, global_context); var list = track.Data; current_output = rtrk.Events; ProcessOperations (rtrk, tctx, list, 0, list.Count, null); Sort (current_output); foreach (var ev in current_output) Util.DebugWriter.WriteLine ("{0} {1} {2}", ev.Tick, ev.Operation, ev.Arguments.Count); } }