Ejemplo n.º 1
0
 public override ChartObject DeserializeSubclass(tick_t pos, tick_t dur, BinaryReader reader, ChartEffectTable effects)
 {
     return(new SlamVolumeEvent()
     {
         Position = pos, Volume = reader.ReadSingleBE()
     });
 }
Ejemplo n.º 2
0
 public override ChartObject DeserializeSubclass(tick_t pos, tick_t dur, BinaryReader reader, ChartEffectTable effects)
 {
     return(new LaserApplicationEvent()
     {
         Position = pos, Application = (LaserApplication)reader.ReadUInt8()
     });
 }
Ejemplo n.º 3
0
 public override ChartObject DeserializeSubclass(tick_t pos, tick_t dur, BinaryReader reader, ChartEffectTable effects)
 {
     return(new SpinImpulseEvent()
     {
         Position = pos, Direction = (AngularDirection)reader.ReadUInt8()
     });
 }
Ejemplo n.º 4
0
        public override ChartObject DeserializeSubclass(tick_t pos, tick_t dur, BinaryReader reader, ChartEffectTable effects)
        {
            byte flags = reader.ReadUInt8();

            var obj = new AnalogObject()
            {
                Position = pos, Duration = dur
            };

            if ((flags & 0x01) != 0)
            {
                obj.RangeExtended = true;
            }

            obj.InitialValue = reader.ReadSingleBE();
            obj.FinalValue   = reader.ReadSingleBE();
            obj.Shape        = (CurveShape)reader.ReadUInt8();

            switch (obj.Shape)
            {
            case CurveShape.Linear: break;

            case CurveShape.Cosine:
            case CurveShape.ThreePoint:
                obj.CurveA = reader.ReadSingleBE();
                obj.CurveB = reader.ReadSingleBE();
                break;
            }

            return(obj);
        }
Ejemplo n.º 5
0
        public Entity AddEntity(HybridLabel lane, string entityType, tick_t position, tick_t duration)
        {
            var entity = (Entity)Activator.CreateInstance(Entity.GetEntityTypeById(entityType));

            entity.Position = position;
            entity.Duration = duration;

            AddEntity(lane, entity);
            return(entity);
        }
Ejemplo n.º 6
0
        public override ChartObject DeserializeSubclass(tick_t pos, tick_t dur, BinaryReader reader, ChartEffectTable effects)
        {
            var   laserIndex = (LaserIndex)reader.ReadUInt8();
            float gain       = reader.ReadSingleBE();

            return(new LaserFilterGainEvent()
            {
                Position = pos, LaserIndex = laserIndex, Gain = gain
            });
        }
Ejemplo n.º 7
0
        public override ChartObject DeserializeSubclass(tick_t pos, tick_t dur, BinaryReader reader, ChartEffectTable effects)
        {
            var evt = new SwingImpulseEvent()
            {
                Position = pos
            };

            evt.Direction = (AngularDirection)reader.ReadUInt8();
            evt.Amplitude = reader.ReadSingleBE();
            return(evt);
        }
Ejemplo n.º 8
0
        public override ChartObject DeserializeSubclass(tick_t pos, tick_t dur, BinaryReader reader, ChartEffectTable effects)
        {
            var evt = new LaserParamsEvent()
            {
                Position = pos
            };

            evt.LaserIndex      = (LaserIndex)reader.ReadUInt8();
            evt.Params.Function = (LaserFunction)reader.ReadUInt8();
            evt.Params.Scale    = (LaserScale)reader.ReadUInt8();
            return(evt);
        }
Ejemplo n.º 9
0
        public override ChartObject DeserializeSubclass(tick_t pos, tick_t dur, BinaryReader reader, ChartEffectTable effects)
        {
            var evt = new WobbleImpulseEvent()
            {
                Position = pos
            };

            evt.Direction = (LinearDirection)reader.ReadUInt8();
            evt.Amplitude = reader.ReadSingleBE();
            evt.Frequency = reader.ReadUInt16BE();
            evt.Decay     = (Decay)reader.ReadUInt8();
            return(evt);
        }
Ejemplo n.º 10
0
        public override ChartObject DeserializeSubclass(tick_t pos, tick_t dur, BinaryReader reader, ChartEffectTable effects)
        {
            byte flags     = reader.ReadUInt8();
            bool hasSample = flags != 0;

            var obj = new ButtonObject()
            {
                Position = pos, Duration = dur
            };

            if (hasSample)
            {
                obj.Sample = reader.ReadStringUTF8();
            }

            return(obj);
        }
Ejemplo n.º 11
0
        public ButtonJudge(Chart chart, HybridLabel label)
            : base(chart, label)
        {
            tick_t tickStep   = (Chart.MaxBpm >= 255 ? 2.0 : 1.0) / (4 * 4);
            tick_t tickMargin = 2 * tickStep;

            foreach (var entity in chart[label])
            {
                var button = (ButtonEntity)entity;

                if (button.IsInstant)
                {
                    m_stateTicks.Add(new StateTick(button, button.AbsolutePosition, JudgeState.ChipAwaitPress));
                    m_scoreTicks.Add(new ScoreTick(button, button.AbsolutePosition, TickKind.Chip));
                }
                else
                {
                    m_stateTicks.Add(new StateTick(button, button.AbsolutePosition, JudgeState.HoldAwaitPress));
                    // end state is placed at the last score tick

                    int numTicks = MathL.FloorToInt((double)(button.Duration - tickMargin) / (double)tickStep);
                    if (numTicks <= 0)
                    {
                        m_scoreTicks.Add(new ScoreTick(button, button.AbsolutePosition + button.AbsoluteDuration / 2, TickKind.Hold));
                        m_stateTicks.Add(new StateTick(button, button.AbsolutePosition + button.AbsoluteDuration / 2, JudgeState.HoldAwaitRelease));
                    }
                    else
                    {
                        for (int i = 0; i < numTicks; i++)
                        {
                            tick_t pos = button.Position + tickMargin + tickStep * i;
                            m_scoreTicks.Add(new ScoreTick(button, chart.CalcTimeFromTick(pos), TickKind.Hold));

                            if (i == numTicks - 1)
                            {
                                m_stateTicks.Add(new StateTick(button, chart.CalcTimeFromTick(pos), JudgeState.HoldAwaitRelease));
                            }
                        }
                    }
                }
            }
        }
Ejemplo n.º 12
0
        public override ChartObject DeserializeSubclass(tick_t pos, tick_t dur, BinaryReader reader, ChartEffectTable effects)
        {
            var    laserIndex = (LaserIndex)reader.ReadUInt8();
            ushort effectID   = reader.ReadUInt16BE();

            EffectDef effect;

            if (effectID == ushort.MaxValue)
            {
                effect = null;
            }
            else
            {
                effect = effects[effectID];
            }

            return(new LaserFilterKindEvent()
            {
                Position = pos, LaserIndex = laserIndex, Effect = effect
            });
        }
Ejemplo n.º 13
0
        protected override void ObjectEnteredJudgement(ChartObject obj)
        {
            if (AutoPlay && !obj.IsInstant)
            {
                m_ticks.Add(new Tick(obj, obj.AbsolutePosition, true, true));
            }

            if (obj.IsInstant)
            {
                var chipTick = new Tick(obj, obj.AbsolutePosition, false);
                m_ticks.Add(chipTick);
            }
            else
            {
                tick_t step   = (Chart.MaxBpm >= 255 ? 2.0 : 1.0) / (4 * 4);
                tick_t margin = 2 * step;

                int numTicks = MathL.FloorToInt((double)(obj.Duration - margin) / (double)step);

                if (numTicks == 0)
                {
                    m_ticks.Add(new Tick(obj, obj.AbsolutePosition + obj.AbsoluteDuration / 2, true));
                }
                else
                {
                    tick_t pos = obj.Position + margin;
                    for (int i = 0; i < numTicks; i++)
                    {
                        time_t timeAtTick = Chart.CalcTimeFromTick(pos + i * step);
                        m_ticks.Add(new Tick(obj, timeAtTick, true));
                    }
                }
            }

            if (AutoPlay && !obj.IsInstant)
            {
                m_ticks.Add(new Tick(obj, obj.AbsoluteEndPosition, true, true));
            }
        }
Ejemplo n.º 14
0
 public TempButtonState(tick_t pos)
 {
     StartPosition = pos;
 }
Ejemplo n.º 15
0
 public void RemoveEntityAtTick(HybridLabel lane, tick_t tick) => Chart[lane].Remove(Chart[lane].Find(tick, false));
Ejemplo n.º 16
0
 public abstract ChartObject DeserializeSubclass(tick_t pos, tick_t dur, BinaryReader reader, ChartEffectTable effects);
Ejemplo n.º 17
0
 public void ForEachEntityInRangeTicks(HybridLabel laneLabel, tick_t startTick, tick_t endTick, bool includeDuration, DynValue function) =>
 Chart[laneLabel].ForEachInRange(startTick, endTick, includeDuration, entity => Script.Call(function, entity));
Ejemplo n.º 18
0
 public Entity?GetEntityAtTick(HybridLabel laneLabel, tick_t tick, bool includeDuration) => Chart[laneLabel].Find(tick, includeDuration);
Ejemplo n.º 19
0
 public Entity?GetEntityAtTick(HybridLabel laneLabel, tick_t tick) => Chart[laneLabel].Find(tick, true);
Ejemplo n.º 20
0
 public time_t CalcTimeFromTick(tick_t tick) => Chart.CalcTimeFromTick(tick);
        public Chart DeserializeChart(ChartInfo chartInfo, Stream inStream)
        {
            var reader = new BinaryReader(inStream, Encoding.UTF8);

            uint magicCheck = reader.ReadUInt32BE();

            if (magicCheck != MAGIC)
            {
                throw new ChartFormatException($"Invalid input stream given.");
            }

            uint versionCheck = reader.ReadUInt8();

            if (versionCheck > VERSION)
            {
                throw new ChartFormatException($"Input stream cannot be read by this serializer: the version is too high.");
            }

            ushort streamCount = reader.ReadUInt16BE();
            var    chart       = new Chart(streamCount)
            {
                Info = chartInfo
            };

            chart.Offset = chartInfo.ChartOffset;

            int effectCount = reader.ReadUInt16BE();
            var effectTable = new ChartEffectTable();

            for (int i = 0; i < effectCount; i++)
            {
                var effect = DeserializeEffectDef(reader);
                effectTable.Add(effect);
            }

            ushort controlPointCount = reader.ReadUInt16BE();

            for (int i = 0; i < controlPointCount; i++)
            {
                tick_t position  = reader.ReadDoubleBE();
                double bpm       = reader.ReadDoubleBE();
                int    beatCount = reader.ReadUInt8();
                int    beatKind  = reader.ReadUInt8();
                double mult      = reader.ReadDoubleBE();

                var cp = chart.ControlPoints.GetOrCreate(position, false);
                cp.BeatsPerMinute  = bpm;
                cp.BeatCount       = beatCount;
                cp.BeatKind        = beatKind;
                cp.SpeedMultiplier = mult;
            }

            for (int s = 0; s < streamCount; s++)
            {
                var stream = chart[s];

                uint ucount = reader.ReadUInt32BE();
                if (ucount > int.MaxValue)
                {
                    throw new ChartFormatException($"Too many objects declared in stream { s }.");
                }
                int count = (int)ucount;

                for (int i = 0; i < count; i++)
                {
                    byte objId      = reader.ReadUInt8();
                    var  serializer = GetSerializerByID(objId);

                    byte flags       = reader.ReadUInt8();
                    bool hasDuration = (flags & 0x01) != 0;

                    tick_t position = reader.ReadDoubleBE();
                    tick_t duration = hasDuration ? reader.ReadDoubleBE() : 0;

                    ChartObject obj;
                    if (serializer != null)
                    {
                        obj = serializer.DeserializeSubclass(position, duration, reader, effectTable);
                    }
                    else
                    {
                        obj = new ChartObject()
                        {
                            Position = position, Duration = duration,
                        }
                    };

                    stream.Add(obj);
                }
            }

            return(chart);
        }
Ejemplo n.º 22
0
        public static Chart CreateChartFromXml(Stream inStream)
        {
            using var reader = XmlReader.Create(inStream);

            var chart = MusecloneChartFactory.Instance.CreateNew();

            var       timingInfo = new Dictionary <int, TimingInfo>();
            var       eventInfos = new List <EventInfo>();
            EventInfo?curEvent   = null;

            reader.MoveToContent();

            #region Read Timing Information and Event Creation

            while (reader.Read())
            {
                switch (reader.NodeType)
                {
                case XmlNodeType.EndElement:
                    if (reader.Name == "event")
                    {
                        //Logger.Log($"End event block: {curEvent!.StartTimeMillis}, {curEvent!.EndTimeMillis}, {curEvent!.Type}, {curEvent!.Kind}");

                        eventInfos.Add(curEvent !);
                        curEvent = null;
                    }
                    break;

                case XmlNodeType.Element:
                {
                    if (reader.Name == "tempo")
                    {
                        int  time = 0, deltaTime = 0, value = 500_000;
                        long bpm = 120_00;
                        while (reader.Read())
                        {
                            if (reader.NodeType == XmlNodeType.Element)
                            {
                                switch (reader.Name)
                                {
                                case "time":
                                {
                                    //string type = reader["__type"];
                                    reader.Read();         // <time ...>
                                    time = reader.ReadContentAsInt();
                                } break;

                                case "delta_time":
                                {
                                    //string type = reader["__type"];
                                    reader.Read();         // <delta_time ...>
                                    deltaTime = reader.ReadContentAsInt();
                                } break;

                                case "val":
                                {
                                    //string type = reader["__type"];
                                    reader.Read();         // <delta_time ...>
                                    value = reader.ReadContentAsInt();
                                } break;

                                case "bpm":
                                {
                                    //string type = reader["__type"];
                                    reader.Read();         // <delta_time ...>
                                    bpm = reader.ReadContentAsLong();
                                } break;
                                }
                            }
                            else if (reader.NodeType == XmlNodeType.EndElement && reader.Name == "tempo")
                            {
                                //Logger.Log($"End tempo block: {time}, {deltaTime}, {value}, {bpm}");

                                if (!timingInfo.TryGetValue(time, out var info))
                                {
                                    timingInfo[time] = info = new TimingInfo();
                                }
                                info.MusecaWhen     = time;
                                info.BeatsPerMinute = bpm / 100.0;
                                break;
                            }
                        }
                    }
                    else if (reader.Name == "sig_info")
                    {
                        int time = 0, deltaTime = 0, num = 4, denomi = 4;
                        while (reader.Read())
                        {
                            if (reader.NodeType == XmlNodeType.Element)
                            {
                                switch (reader.Name)
                                {
                                case "time":
                                {
                                    //string type = reader["__type"];
                                    reader.Read();         // <time ...>
                                    time = reader.ReadContentAsInt();
                                }
                                break;

                                case "delta_time":
                                {
                                    //string type = reader["__type"];
                                    reader.Read();         // <delta_time ...>
                                    deltaTime = reader.ReadContentAsInt();
                                }
                                break;

                                case "num":
                                {
                                    //string type = reader["__type"];
                                    reader.Read();         // <delta_time ...>
                                    num = reader.ReadContentAsInt();
                                }
                                break;

                                case "denomi":
                                {
                                    //string type = reader["__type"];
                                    reader.Read();         // <delta_time ...>
                                    denomi = reader.ReadContentAsInt();
                                }
                                break;
                                }
                            }
                            else if (reader.NodeType == XmlNodeType.EndElement && reader.Name == "sig_info")
                            {
                                //Logger.Log($"End sig_info block: {time}, {deltaTime}, {num}, {denomi}");

                                if (!timingInfo.TryGetValue(time, out var info))
                                {
                                    timingInfo[time] = info = new TimingInfo();
                                }
                                info.Numerator   = num;
                                info.Denominator = denomi;
                                break;
                            }
                        }
                    }

                    switch (reader.Name)
                    {
                    case "event": curEvent = new EventInfo(); break;

                    case "stime_ms":
                    {
                        reader.Read();
                        curEvent !.StartTimeMillis = reader.ReadContentAsLong();
                    }
                    break;

                    case "etime_ms":
                    {
                        reader.Read();
                        curEvent !.EndTimeMillis = reader.ReadContentAsLong();
                    }
                    break;

                    case "type":
                    {
                        reader.Read();
                        curEvent !.Type = reader.ReadContentAsInt();
                    }
                    break;

                    case "kind":
                    {
                        reader.Read();
                        curEvent !.Kind = reader.ReadContentAsInt();
                    }
                    break;
                    }
                } break;
                }
            }

            #endregion

            #region Construct :theori Timing Data

            foreach (var info in from pair in timingInfo
                     orderby pair.Key select pair.Value)
            {
                tick_t position = chart.CalcTickFromTime(info.MusecaWhen / 1000.0);

                var cp = chart.ControlPoints.GetOrCreate(position, true);
                if (info.BeatsPerMinute > 0)
                {
                    cp.BeatsPerMinute = info.BeatsPerMinute;
                }

                if (info.Numerator != 0 && info.Denominator != 0 && (cp.BeatCount != info.Numerator || cp.BeatKind != info.Denominator))
                {
                    cp           = chart.ControlPoints.GetOrCreate(MathL.Ceil((double)position), true);
                    cp.BeatCount = info.Numerator;
                    cp.BeatKind  = info.Denominator;
                }
            }

            #endregion

            #region Determine timing info from beat events

            if (false && timingInfo.Count == 0)
            {
                var barEvents = from e in eventInfos where e.Kind == 11 || e.Kind == 12 select e;

                // bpm related
                long lastBeatDurationMillis = 0, lastBeatStartMillis = 0, lastBpmStartMillis = 0;
                int  bpmTotalBeats            = 0;
                long runningBeatDurationTotal = 0;
                int  numBeatsCounted          = 0;

                // sig related
                int    n = 4;
                int    measure = 0, totalMeasures = 0, beatCount = 0, totalBeatsInMeasure = 0;
                long   sigMeasureStartMillis = 0, sigCurrentMeasureStartMillis = 0, sigOffsetMillis = 0;
                tick_t offset = 0;

                var bpmChanges     = new Dictionary <int, (long Millis, double BeatsPerMinute)>();
                var timeSigChanges = new Dictionary <int, (int Numerator, int Denominator)>();

                foreach (var e in barEvents)
                {
                    long millis = e.StartTimeMillis;

                    if (totalBeatsInMeasure > 0)
                    {
                        lastBpmStartMillis = lastBeatStartMillis;

                        long beatDuration = millis - lastBeatStartMillis;
                        if (lastBeatDurationMillis != 0)
                        {
                            // TODO(local): if this is within like a millisecond then maybe it's fine
                            if (Math.Abs(beatDuration - lastBeatDurationMillis) > 10)
                            {
                                bpmChanges[bpmTotalBeats] = (lastBpmStartMillis, 60_000.0 / (runningBeatDurationTotal / (double)numBeatsCounted));

                                lastBpmStartMillis = lastBeatStartMillis;

                                numBeatsCounted          = 0;
                                runningBeatDurationTotal = 0;
                            }
                        }

                        lastBeatDurationMillis = beatDuration;
                    }

                    bpmTotalBeats++;
                    numBeatsCounted++;
                    runningBeatDurationTotal += millis - lastBeatStartMillis;
                    lastBeatStartMillis       = millis;

                    if (e.Kind == 11) // measure marker
                    {
                        totalMeasures++;

                        if (totalBeatsInMeasure == 0) // first one in the chart
                        {
                            sigOffsetMillis       = millis;
                            sigMeasureStartMillis = millis;

                            beatCount++;
                            totalBeatsInMeasure++;
                        }
                        else // check that the previous beat count matches `n`
                        {
                            if (beatCount != n)
                            {
                                timeSigChanges[totalMeasures - 1] = (beatCount, 4);

                                totalBeatsInMeasure = 0;
                                measure             = 0;
                            }

                            // continue as normal
                            totalBeatsInMeasure++;
                            measure++;

                            beatCount = 1;
                            sigCurrentMeasureStartMillis = millis;
                        }
                    }
                    else // beat marker
                    {
                        beatCount++;
                        totalBeatsInMeasure++;
                    }
                }

                bpmChanges[bpmTotalBeats] = (lastBeatStartMillis, 60_000.0 / (runningBeatDurationTotal / (double)numBeatsCounted));

                chart.Offset = sigOffsetMillis / 1_000.0;
                foreach (var(measureIndex, (num, denom)) in timeSigChanges)
                {
                    tick_t position = measureIndex;
                    var    cp       = chart.ControlPoints.GetOrCreate(position, true);
                    cp.BeatCount = num;
                    cp.BeatKind  = denom;
                }

                foreach (var(beatIndex, (timeMillis, bpm)) in bpmChanges)
                {
                    int beatsLeft = beatIndex;
                    tick_t? where = null;

                    foreach (var cp in chart.ControlPoints)
                    {
                        if (!cp.HasNext)
                        {
                            where = cp.Position + (double)beatsLeft / cp.BeatCount;
                            break;
                        }
                        else
                        {
                            int numBeatsInCp = (int)(cp.BeatCount * (double)(cp.Next.Position - cp.Position));
                            if (beatsLeft > numBeatsInCp)
                            {
                                beatsLeft -= numBeatsInCp;
                            }
                            else
                            {
                                where = cp.Position + (double)beatsLeft / cp.BeatCount;
                                break;
                            }
                        }
                    }

                    if (where.HasValue)
                    {
                        var cp = chart.ControlPoints.GetOrCreate(where.Value, true);
                        cp.BeatsPerMinute = bpm;
                    }
                    //else Logger.Log($"Bpm change at beat {beatIndex} (timeMillis) could not be created for bpm {bpm}");
                }
            }

            #endregion

            var noteInfos = from e in eventInfos where e.Kind != 11 && e.Kind != 12 && e.Kind != 14 && e.Kind != 15 select e;

            foreach (var entity in noteInfos)
            {
                tick_t startTicks = chart.CalcTickFromTime(entity.StartTimeMillis / 1000.0);
                tick_t endTicks   = chart.CalcTickFromTime(entity.EndTimeMillis / 1000.0);

                const int q = 192;
                startTicks = MathL.Round((double)(startTicks * q)) / q;
                endTicks   = MathL.Round((double)(endTicks * q)) / q;

                tick_t durTicks = endTicks - startTicks;

                if (entity.Kind == 1 && entity.Type == 5)
                {
                    chart[5].Add <ButtonEntity>(startTicks, durTicks);
                }
                else
                {
                    switch (entity.Kind)
                    {
                    // "chip" tap note
                    case 0: chart[entity.Type].Add <ButtonEntity>(startTicks); break;

                    // hold tap note, ignore foot pedal bc handled above
                    case 1: chart[entity.Type - 6].Add <ButtonEntity>(startTicks, durTicks); break;

                    // large spinner
                    case 2:
                    {
                        var e = chart[entity.Type % 6].Add <SpinnerEntity>(startTicks, durTicks);
                        e.Large = true;
                    } break;

                    // large spinner left
                    case 3:
                    {
                        var e = chart[entity.Type % 6].Add <SpinnerEntity>(startTicks, durTicks);
                        e.Direction = LinearDirection.Left;
                        e.Large     = true;
                    } break;

                    // large spinner right
                    case 4:
                    {
                        var e = chart[entity.Type % 6].Add <SpinnerEntity>(startTicks, durTicks);
                        e.Direction = LinearDirection.Right;
                        e.Large     = true;
                    } break;

                    // small spinner
                    case 5:
                    {
                        var e = chart[entity.Type].Add <SpinnerEntity>(startTicks);
                    } break;

                    // small spinner left
                    case 6:
                    {
                        var e = chart[entity.Type].Add <SpinnerEntity>(startTicks);
                        e.Direction = LinearDirection.Left;
                    } break;

                    // small spinner right
                    case 7:
                    {
                        var e = chart[entity.Type].Add <SpinnerEntity>(startTicks);
                        e.Direction = LinearDirection.Right;
                    } break;
                    }
                }
            }

            return(chart);
        }
Ejemplo n.º 23
0
 public void WriteValue(tick_t value) => m_writer.WriteValue((double)value);
Ejemplo n.º 24
0
 public TempLaserState(tick_t pos, ControlPoint cp)
 {
     StartPosition = pos;
     ControlPoint  = cp;
 }
Ejemplo n.º 25
0
        public LaserJudge(Chart chart, HybridLabel label)
            : base(chart, label)
        {
            tick_t tickStep = (Chart.MaxBpm >= 255 ? 2.0 : 1.0) / (4 * 4);

            // score ticks first
            foreach (var entity in chart[label])
            {
                var root = (AnalogEntity)entity;
                if (root.PreviousConnected != null)
                {
                    continue;
                }

                // now we're working with the root
                var start = root;
                while (start != null)
                {
                    var next = start;
                    while (next.NextConnected is AnalogEntity a && !a.IsInstant)
                    {
                        next = a;
                    }

                    Debug.Assert(next.Position >= start.Position);
                    if (next.Position > start.Position)
                    {
                        Debug.Assert(!next.IsInstant);
                    }

                    tick_t startPos     = start.Position;
                    bool   endsWithSlam = next.NextConnected is AnalogEntity && next.IsInstant;

                    int numTicks = MathL.Max(1, MathL.FloorToInt((double)(next.EndPosition - startPos) / (double)tickStep));
                    if (endsWithSlam && next.EndPosition - (startPos + tickStep * numTicks) < tickStep)
                    {
                        numTicks--;
                    }

                    for (int i = 0; i < numTicks; i++)
                    {
                        tick_t pos = startPos + i * tickStep;

                        var kind = (i == 0 && start.IsInstant) ? TickKind.Slam : TickKind.Segment;
                        m_scoreTicks.Add(new ScoreTick(root, chart.CalcTimeFromTick(pos), kind));
                    }

                    start = next.NextConnected as AnalogEntity;
                }
            }

            // state ticks seconds
            foreach (var entity in chart[label])
            {
                var root = (AnalogEntity)entity;
                if (root.PreviousConnected != null)
                {
                    continue;
                }

                time_t cursorResetTime = root.AbsolutePosition - m_cursorResetDistance;
                time_t laserBeginTime  = root.AbsolutePosition - m_directionChangeRadius;

                if (root.Previous is AnalogEntity p)
                {
                    cursorResetTime = MathL.Max((double)p.AbsoluteEndPosition, (double)cursorResetTime);
                    laserBeginTime  = MathL.Max((double)p.AbsoluteEndPosition, (double)laserBeginTime);
                }

                m_stateTicks.Add(new StateTick(root, root, cursorResetTime, JudgeState.CursorReset));
                m_stateTicks.Add(new StateTick(root, root, laserBeginTime, JudgeState.LaserBegin));

                if (root.DirectionSign != 0)
                {
                    m_stateTicks.Add(new StateTick(root, root, root.AbsolutePosition, JudgeState.SwitchDirection));
                }

                if (root.NextConnected is AnalogEntity segment)
                {
                    while (segment != null)
                    {
                        if (segment.DirectionSign != ((AnalogEntity)segment.Previous).DirectionSign)
                        {
                            m_stateTicks.Add(new StateTick(root, segment, segment.AbsolutePosition, JudgeState.SwitchDirection));
                        }
                        else if (segment.IsInstant)
                        {
                            m_stateTicks.Add(new StateTick(root, segment, segment.AbsolutePosition, JudgeState.SameDirectionSlam));
                        }

                        if (segment.NextConnected == null)
                        {
                            m_stateTicks.Add(new StateTick(root, segment, segment.AbsoluteEndPosition, JudgeState.LaserEnd));
                        }
                        segment = segment.NextConnected as AnalogEntity;
                    }
                }
                else
                {
                    m_stateTicks.Add(new StateTick(root, root, root.AbsoluteEndPosition, JudgeState.LaserEnd));
                }
            }
        }
Ejemplo n.º 26
0
        public static Chart ToVoltex(this KshChart ksh)
        {
            Logger.Log("ksh.convert start");

            bool hasActiveEffects = !(ksh.Metadata.MusicFile != null && ksh.Metadata.MusicFileNoFx != null);

            Logger.Log("ksh.convert effects disabled");

            var chart = new Chart(StreamIndex.COUNT)
            {
                Offset = ksh.Metadata.OffsetMillis / 1_000.0
            };

            chart.Info = new ChartInfo()
            {
                SongTitle           = ksh.Metadata.Title,
                SongArtist          = ksh.Metadata.Artist,
                SongFileName        = ksh.Metadata.MusicFile ?? ksh.Metadata.MusicFileNoFx,
                SongVolume          = ksh.Metadata.MusicVolume,
                ChartOffset         = chart.Offset,
                Charter             = ksh.Metadata.EffectedBy,
                JacketFileName      = ksh.Metadata.JacketPath,
                JacketArtist        = ksh.Metadata.Illustrator,
                BackgroundFileName  = ksh.Metadata.Background,
                BackgroundArtist    = "Unknown",
                DifficultyLevel     = ksh.Metadata.Level,
                DifficultyIndex     = ksh.Metadata.Difficulty.ToDifficultyIndex(ksh.FileName),
                DifficultyName      = ksh.Metadata.Difficulty.ToDifficultyString(ksh.FileName),
                DifficultyNameShort = ksh.Metadata.Difficulty.ToShortString(ksh.FileName),
                DifficultyColor     = ksh.Metadata.Difficulty.GetColor(ksh.FileName),
            };

            {
                if (double.TryParse(ksh.Metadata.BeatsPerMinute, out double bpm))
                {
                    chart.ControlPoints.Root.BeatsPerMinute = bpm;
                }

                var laserParams = chart[StreamIndex.LaserParams].Add <LaserParamsEvent>(0);
                laserParams.LaserIndex = LaserIndex.Both;

                var laserGain = chart[StreamIndex.LaserFilterGain].Add <LaserFilterGainEvent>(0);
                laserGain.LaserIndex = LaserIndex.Both;
                if (!hasActiveEffects)
                {
                    laserGain.Gain = 0.0f;
                }
                else
                {
                    laserGain.Gain = ksh.Metadata.PFilterGain / 100.0f;
                }

                var laserFilter = chart[StreamIndex.LaserFilterKind].Add <LaserFilterKindEvent>(0);
                laserFilter.LaserIndex = LaserIndex.Both;
                laserFilter.Effect     = ksh.FilterDefines[ksh.Metadata.FilterType];

                var slamVolume = chart[StreamIndex.SlamVolume].Add <SlamVolumeEvent>(0);
                if (!hasActiveEffects)
                {
                    slamVolume.Volume = 0.0f;
                }
                else
                {
                    slamVolume.Volume = ksh.Metadata.SlamVolume / 100.0f;
                }
            }

            var lastCp = chart.ControlPoints.Root;

            var buttonStates = new TempButtonState[6];
            var laserStates  = new TempLaserState[2];

            bool[] laserIsExtended = new bool[2] {
                false, false
            };
            PathPointEvent lastTiltEvent = null;

            foreach (var tickRef in ksh)
            {
                var tick = tickRef.Tick;

                int    blockOffset = tickRef.Block;
                tick_t chartPos    = blockOffset + (double)tickRef.Index / tickRef.MaxIndex;

                foreach (var setting in tick.Settings)
                {
                    string key = setting.Key;
                    switch (key)
                    {
                    case "beat":
                    {
                        if (!setting.Value.ToString().TrySplit('/', out string n, out string d))
                        {
                            n = d = "4";
                            Logger.Log($"ksh.convert error: { setting.Value } is not a valid time signature. Defaulting to 4/4.");
                        }

                        tick_t       pos = MathL.Ceil((double)chartPos);
                        ControlPoint cp  = chart.ControlPoints.GetOrCreate(pos, true);
                        cp.BeatCount = int.Parse(n);
                        cp.BeatKind  = int.Parse(d);
                        lastCp       = cp;

                        Logger.Log($"ksh.convert time signature { cp.BeatCount }/{ cp.BeatKind }");
                    } break;

                    case "t":
                    {
                        ControlPoint cp = chart.ControlPoints.GetOrCreate(chartPos, true);
                        cp.BeatsPerMinute = double.Parse(setting.Value.ToString());
                        lastCp            = cp;
                        Logger.Log($"ksh.convert bpm { cp.BeatsPerMinute }");
                    } break;

                    case "fx-l":
                    case "fx-r":
                    {
                        if (hasActiveEffects)
                        {
                            var effectEvent = chart[StreamIndex.EffectKind].Add <EffectKindEvent>(chartPos);
                            effectEvent.EffectIndex = key == "fx-l" ? 4 : 5;
                            effectEvent.Effect      = (string)setting.Value.Value == "" ? null : ksh.FxDefines[setting.Value.ToString()];
                            Logger.Log($"ksh.convert set { key } { effectEvent.Effect?.GetType().Name ?? "nothing" }");
                        }
                        else
                        {
                            Logger.Log($"ksh.convert effects disabled for { key }");
                        }
                    } break;

                    case "fx-l_param1":
                    {
                        Logger.Log($"ksh.convert skipping fx-l_param1.");
                    } break;

                    case "fx-r_param1":
                    {
                        Logger.Log($"ksh.convert skipping fx-r_param1.");
                    } break;

                    case "pfiltergain":
                    {
                        if (hasActiveEffects)
                        {
                            var laserGain = chart[StreamIndex.LaserFilterGain].Add <LaserFilterGainEvent>(chartPos);
                            laserGain.LaserIndex = LaserIndex.Both;
                            laserGain.Gain       = setting.Value.ToInt() / 100.0f;
                            Logger.Log($"ksh.convert set { key } { setting.Value }");
                        }
                        else
                        {
                            Logger.Log($"ksh.convert effects disabled for { key }");
                        }
                    } break;

                    case "filtertype":
                    {
                        if (hasActiveEffects)
                        {
                            var laserFilter = chart[StreamIndex.LaserFilterKind].Add <LaserFilterKindEvent>(chartPos);
                            laserFilter.LaserIndex = LaserIndex.Both;
                            laserFilter.Effect     = (string)setting.Value.Value == "" ? null : ksh.FilterDefines[setting.Value.ToString()];
                            Logger.Log($"ksh.convert set { key } { laserFilter.Effect?.GetType().Name ?? "nothing" }");
                        }
                        else
                        {
                            Logger.Log($"ksh.convert effects disabled for { key }");
                        }
                    }
                    break;

                    case "filter-l":     // NOTE(local): This is an extension, not originally supported in KSH. Used primarily for development purposes, but may also be exported to KSH should someone want to export back to that format.
                    case "filter-r":     // NOTE(local): This is an extension, not originally supported in KSH. Used primarily for development purposes, but may also be exported to KSH should someone want to export back to that format.
                    {
                        Logger.Log($"ksh.convert skipping { key }.");
                    }
                    break;

                    case "filter-l_gain":     // NOTE(local): This is an extension, not originally supported in KSH. Used primarily for development purposes, but may also be exported to KSH should someone want to export back to that format.
                    case "filter-r_gain":     // NOTE(local): This is an extension, not originally supported in KSH. Used primarily for development purposes, but may also be exported to KSH should someone want to export back to that format.
                    {
                        Logger.Log($"ksh.convert skipping { key }.");
                    }
                    break;

                    case "chokkakuvol":
                    {
                        if (hasActiveEffects)
                        {
                            var slamVoume = chart[StreamIndex.SlamVolume].Add <SlamVolumeEvent>(chartPos);
                            slamVoume.Volume = setting.Value.ToInt() / 100.0f;
                            Logger.Log($"ksh.convert set { key } { setting.Value }");
                        }
                        else
                        {
                            Logger.Log($"ksh.convert effects disabled for { key }");
                        }
                    } break;

                    case "laserrange_l": { laserIsExtended[0] = true; } break;

                    case "laserrange_r": { laserIsExtended[1] = true; } break;

                    case "zoom_bottom":
                    {
                        var point = chart[StreamIndex.Zoom].Add <PathPointEvent>(chartPos);
                        point.Value = setting.Value.ToInt() / 100.0f;
                        Logger.Log($"ksh.convert zoom { setting.Value }");
                    } break;

                    case "zoom_top":
                    {
                        var point = chart[StreamIndex.Pitch].Add <PathPointEvent>(chartPos);
                        point.Value = setting.Value.ToInt() / 100.0f;
                        Logger.Log($"ksh.convert pitch { setting.Value }");
                    } break;

                    case "zoom_side":
                    {
                        var point = chart[StreamIndex.Offset].Add <PathPointEvent>(chartPos);
                        point.Value = setting.Value.ToInt() / 100.0f;
                        Logger.Log($"ksh.convert offset { setting.Value }");
                    } break;

                    case "roll":     // NOTE(local): This is an extension, not originally supported in KSH. Used primarily for development purposes, but may also be exported to KSH should someone want to export back to that format.
                    {
                        var point = chart[StreamIndex.Roll].Add <PathPointEvent>(chartPos);
                        point.Value = setting.Value.ToInt() / 360.0f;
                        Logger.Log($"ksh.convert custom manual tilt { setting.Value }");
                    } break;

                    case "tilt":
                    {
                        var laserApps = chart[StreamIndex.LaserApplication].Add <LaserApplicationEvent>(chartPos);

                        string v = setting.Value.ToString();
                        if (v.StartsWith("keep_"))
                        {
                            laserApps.Application = LaserApplication.Additive | LaserApplication.KeepMax;
                            v = v.Substring(5);
                        }

                        var laserParams = chart[StreamIndex.LaserParams].Add <LaserParamsEvent>(chartPos);
                        laserParams.LaserIndex = LaserIndex.Both;

                        bool disableTilt = true;
                        switch (v)
                        {
                        default:
                        {
                            if (int.TryParse(v, out int manualValue))
                            {
                                disableTilt = false;

                                if (lastTiltEvent == null)
                                {
                                    var startPoint = chart[StreamIndex.Roll].Add <PathPointEvent>(chartPos);
                                    startPoint.Value = 0;
                                }

                                var point = chart[StreamIndex.Roll].Add <PathPointEvent>(chartPos);
                                point.Value = -manualValue * 14 / 360.0f;

                                lastTiltEvent = point;
                            }
                        } goto case "zero";

                        case "zero": laserParams.Params.Function = LaserFunction.Zero; break;

                        case "normal": laserParams.Params.Scale = LaserScale.Normal; break;

                        case "bigger": laserParams.Params.Scale = LaserScale.Bigger; break;

                        case "biggest": laserParams.Params.Scale = LaserScale.Biggest; break;
                        }

                        if (disableTilt && lastTiltEvent != null)
                        {
                        }
                    } break;

                    case "fx_sample":
                    {
                        Logger.Log($"ksh.convert skipping fx_sample.");
                    } break;

                    case "stop":
                    {
                        Logger.Log($"ksh.convert skipping stop.");
                    } break;

                    case "lane_toggle":
                    {
                        Logger.Log($"ksh.convert skipping lane_toggle.");
                    } break;
                    }
                }

                for (int b = 0; b < 6; b++)
                {
                    bool isFx = b >= 4;

                    var data   = isFx ? tick.Fx[b - 4] : tick.Bt[b];
                    var fxKind = data.FxKind;

                    void CreateHold(tick_t endPos)
                    {
                        var state = buttonStates[b];

                        var startPos = state.StartPosition;
                        var button   = chart[b].Add <ButtonObject>(startPos, endPos - startPos);
                    }

                    switch (data.State)
                    {
                    case KshButtonState.Off:
                    {
                        if (buttonStates[b] != null)
                        {
                            CreateHold(chartPos);
                        }
                        buttonStates[b] = null;
                    } break;

                    case KshButtonState.Chip:
                    case KshButtonState.ChipSample:
                    {
                        //System.Diagnostics.Trace.WriteLine(b);
                        chart[b].Add <ButtonObject>(chartPos);
                    } break;

                    case KshButtonState.Hold:
                    {
                        if (buttonStates[b] == null)
                        {
                            buttonStates[b] = new TempButtonState(chartPos);
                        }
                    } break;
                    }
                }

                for (int l = 0; l < 2; l++)
                {
                    var data  = tick.Laser[l];
                    var state = data.State;

                    tick_t CreateSegment(tick_t endPos, float endAlpha)
                    {
                        var   startPos   = laserStates[l].StartPosition;
                        float startAlpha = laserStates[l].StartAlpha;

                        var duration = endPos - startPos;

                        //if (duration <= tick_t.FromFraction(1, 32 * lastCp.BeatCount / lastCp.BeatKind))
                        if (laserStates[l].HiResTickCount <= 6 && startAlpha != endAlpha)
                        {
                            duration = 0;

                            if (laserStates[l].PreviousSlamDuration != 0)
                            {
                                var cDuration = laserStates[l].PreviousSlamDuration;

                                var connector = chart[l + 6].Add <AnalogObject>(startPos, cDuration);
                                connector.InitialValue  = startAlpha;
                                connector.FinalValue    = startAlpha;
                                connector.RangeExtended = laserIsExtended[l];

                                startPos += cDuration;
                            }
                        }

                        var analog = chart[l + 6].Add <AnalogObject>(startPos, duration);

                        analog.InitialValue  = startAlpha;
                        analog.FinalValue    = endAlpha;
                        analog.RangeExtended = laserIsExtended[l];
                        analog.Shape         = laserStates[l].Shape;
                        analog.CurveA        = laserStates[l].CurveA;
                        analog.CurveB        = laserStates[l].CurveB;

                        return(startPos + duration);
                    }

                    switch (state)
                    {
                    case KshLaserState.Inactive:
                    {
                        if (laserStates[l] != null)
                        {
                            laserStates[l]     = null;
                            laserIsExtended[l] = false;
                        }
                    } break;

                    case KshLaserState.Lerp:
                    {
                        laserStates[l].HiResTickCount += (192 * lastCp.BeatCount / lastCp.BeatKind) / tickRef.MaxIndex;
                    } break;

                    case KshLaserState.Position:
                    {
                        var alpha    = data.Position;
                        var startPos = chartPos;

                        tick_t prevSlamDuration = 0;
                        if (laserStates[l] != null)
                        {
                            startPos = CreateSegment(chartPos, alpha.Alpha);
                            if (startPos != chartPos)
                            {
                                prevSlamDuration = chartPos - startPos;
                            }
                        }

                        var ls = laserStates[l] = new TempLaserState(startPos, lastCp)
                        {
                            StartAlpha           = alpha.Alpha,
                            HiResTickCount       = (192 * lastCp.BeatCount / lastCp.BeatKind) / tickRef.MaxIndex,
                            PreviousSlamDuration = prevSlamDuration,
                        };

                        for (int i = tick.Comments.Count - 1; i >= 0; i--)
                        {
                            string c = tick.Comments[i];
                            if (!c.StartsWith("LaserShape "))
                            {
                                continue;
                            }
                            c = c.Substring("LaserShape ".Length).Trim();

                            if (c.StartsWith("ThreePoint"))
                            {
                                float a = 0.5f, b = 0.5f;
                                if (c != "ThreePoint" && c.TrySplit(' ', out string tp, out string sa, out string sb))
                                {
                                    float.TryParse(sa, out a);
                                    float.TryParse(sb, out b);
                                }

                                ls.Shape  = CurveShape.ThreePoint;
                                ls.CurveA = a;
                                ls.CurveB = b;
                            }
                            else if (c == "Cosine")
                            {
                                ls.Shape = CurveShape.Cosine;
                            }
                            else
                            {
                                continue;
                            }
                            break;
                        }
                    } break;
                    }
                }

                switch (tick.Add.Kind)
                {
                case KshAddKind.None: break;

                case KshAddKind.Spin:
                {
                    tick_t duration = tick_t.FromFraction(tick.Add.Duration * 2, 192);
                    var    spin     = chart[StreamIndex.HighwayEffect].Add <SpinImpulseEvent>(chartPos, duration);
                    spin.Direction = (AngularDirection)tick.Add.Direction;
                } break;

                case KshAddKind.Swing:
                {
                    tick_t duration = tick_t.FromFraction(tick.Add.Duration * 2, 192);
                    var    swing    = chart[StreamIndex.HighwayEffect].Add <SwingImpulseEvent>(chartPos, duration);
                    swing.Direction = (AngularDirection)tick.Add.Direction;
                    swing.Amplitude = tick.Add.Amplitude * 70 / 100.0f;
                } break;

                case KshAddKind.Wobble:
                {
                    tick_t duration = tick_t.FromFraction(tick.Add.Duration, 192);
                    var    wobble   = chart[StreamIndex.HighwayEffect].Add <WobbleImpulseEvent>(chartPos, duration);
                    wobble.Direction = (LinearDirection)tick.Add.Direction;
                    wobble.Amplitude = tick.Add.Amplitude / 250.0f;
                    wobble.Decay     = (Decay)tick.Add.Decay;
                    wobble.Frequency = tick.Add.Frequency;
                } break;
                }
            }

            Logger.Log("ksh.convert end");
            return(chart);
        }
    }
Ejemplo n.º 27
0
        public static Chart ToVoltex(this KshChart ksh, ChartInfo?info = null)
        {
            Logger.Log($"ksh.convert start");

            bool hasActiveEffects = !(ksh.Metadata.MusicFile != null && ksh.Metadata.MusicFileNoFx != null);

            Logger.Log($"ksh.convert effects disabled");

            var chart = NeuroSonicChartFactory.Instance.CreateNew();

            chart.Offset = (ksh.Metadata.OffsetMillis) / 1_000.0;

            // if info is non-null, set information exists as well.
            chart.Info = info ?? new ChartInfo()
            {
                SongTitle           = ksh.Metadata.Title,
                SongArtist          = ksh.Metadata.Artist,
                SongFileName        = ksh.Metadata.MusicFile ?? ksh.Metadata.MusicFileNoFx ?? "??",
                SongVolume          = ksh.Metadata.MusicVolume,
                ChartOffset         = chart.Offset,
                Charter             = ksh.Metadata.EffectedBy,
                JacketFileName      = ksh.Metadata.JacketPath,
                JacketArtist        = ksh.Metadata.Illustrator,
                BackgroundFileName  = ksh.Metadata.Background,
                BackgroundArtist    = "Unknown",
                DifficultyLevel     = ksh.Metadata.Level,
                DifficultyIndex     = ksh.Metadata.Difficulty.ToDifficultyIndex(ksh.FileName),
                DifficultyName      = ksh.Metadata.Difficulty.ToDifficultyString(ksh.FileName),
                DifficultyNameShort = ksh.Metadata.Difficulty.ToShortString(ksh.FileName),
                DifficultyColor     = ksh.Metadata.Difficulty.GetColor(ksh.FileName),
            };

            {
                if (double.TryParse(ksh.Metadata.BeatsPerMinute, out double bpm))
                {
                    chart.ControlPoints.Root.BeatsPerMinute = bpm;
                }

                var laserParams = chart[NscLane.LaserEvent].Add <LaserParamsEvent>(0);
                laserParams.LaserIndex = LaserIndex.Both;

                var laserGain = chart[NscLane.LaserEvent].Add <LaserFilterGainEvent>(0);
                laserGain.LaserIndex = LaserIndex.Both;
                if (!hasActiveEffects)
                {
                    laserGain.Gain = 0.0f;
                }
                else
                {
                    laserGain.Gain = ksh.Metadata.PFilterGain / 100.0f;
                }

                var laserFilter = chart[NscLane.LaserEvent].Add <LaserFilterKindEvent>(0);
                laserFilter.LaserIndex = LaserIndex.Both;
                laserFilter.Effect     = new KshEffectRef(ksh.Metadata.FilterType, null).CreateEffectDef(ksh.FilterDefines);

                var slamVolume = chart[NscLane.LaserEvent].Add <SlamVolumeEvent>(0);
                slamVolume.Volume = ksh.Metadata.SlamVolume / 100.0f;
            }

            double modeBpm;

            if (ksh.Metadata.HiSpeedBpm != null)
            {
                modeBpm = ksh.Metadata.HiSpeedBpm.Value;
            }
            else
            {
                var    bpms     = new List <(tick_t, double)>();
                tick_t lastTick = 0;

                foreach (var tickRef in ksh)
                {
                    var tick = tickRef.Tick;

                    int    blockOffset = tickRef.Block;
                    tick_t chartPos    = blockOffset + (double)tickRef.Index / tickRef.MaxIndex;

                    foreach (var setting in tick.Settings)
                    {
                        if (setting.Key == "t")
                        {
                            double bpm = double.Parse(setting.Value.ToString());
                            if (bpms.Count > 0 && bpm == bpms[bpms.Count - 1].Item2)
                            {
                                continue;
                            }
                            bpms.Add((chartPos, bpm));
                        }
                    }

                    lastTick = chartPos;
                }

                var    bpmDurs = new Dictionary <double, tick_t>();
                tick_t longest = -1;

                double result = 120.0;
                for (int i = 0; i < bpms.Count; i++)
                {
                    bool last = i == bpms.Count - 1;
                    var(when, bpm) = bpms[i];

                    tick_t duration;
                    if (last)
                    {
                        duration = lastTick - when;
                    }
                    else
                    {
                        duration = bpms[i].Item1 - when;
                    }

                    if (bpmDurs.TryGetValue(bpm, out tick_t accum))
                    {
                        bpmDurs[bpm] = accum + duration;
                    }
                    else
                    {
                        bpmDurs[bpm] = duration;
                    }

                    if (bpmDurs[bpm] > longest)
                    {
                        longest = bpmDurs[bpm];
                        result  = bpm;
                    }
                }

                modeBpm = result;
            }

            var lastCp = chart.ControlPoints.Root;

            var buttonStates = new TempButtonState[6];
            var laserStates  = new TempLaserState[2];

            bool[] laserIsExtended = new bool[2] {
                false, false
            };
            GraphPointEvent lastTiltEvent = null;

            foreach (var tickRef in ksh)
            {
                var tick = tickRef.Tick;

                int    blockOffset = tickRef.Block;
                tick_t chartPos    = blockOffset + (double)tickRef.Index / tickRef.MaxIndex;

                string[] chipHitSounds       = new string[6];
                float[]  chipHitSoundsVolume = new float[6];

                foreach (var setting in tick.Settings)
                {
                    string key = setting.Key;
                    switch (key)
                    {
                    case "beat":
                    {
                        if (!setting.Value.ToString().TrySplit('/', out string n, out string d))
                        {
                            n = d = "4";
                            Logger.Log($"ksh.convert({ chartPos }) error: { setting.Value } is not a valid time signature. Defaulting to 4/4.");
                        }

                        tick_t       pos = MathL.Ceil((double)chartPos);
                        ControlPoint cp  = chart.ControlPoints.GetOrCreate(pos, true);
                        cp.BeatCount = int.Parse(n);
                        cp.BeatKind  = int.Parse(d);
                        lastCp       = cp;

                        Logger.Log($"ksh.convert({ chartPos }) time signature { cp.BeatCount }/{ cp.BeatKind }");
                    } break;

                    case "t":
                    {
                        ControlPoint cp = chart.ControlPoints.GetOrCreate(chartPos, true);
                        cp.BeatsPerMinute  = double.Parse(setting.Value.ToString());
                        cp.SpeedMultiplier = cp.BeatsPerMinute / modeBpm;
                        lastCp             = cp;
                        Logger.Log($"ksh.convert({ chartPos }) bpm { cp.BeatsPerMinute }");
                    } break;

                    case "fx-l":
                    case "fx-r":
                    {
                        if (hasActiveEffects)
                        {
                            var effectEvent = chart[NscLane.ButtonEvent].Add <EffectKindEvent>(chartPos);
                            effectEvent.EffectIndex = key == "fx-l" ? 4 : 5;
                            effectEvent.Effect      = (setting.Value.Value as KshEffectRef)?.CreateEffectDef(ksh.FxDefines);
                            Logger.Log($"ksh.convert({ chartPos }) set { key } { effectEvent.Effect?.GetType().Name ?? "nothing" }");
                        }
                        else
                        {
                            Logger.Log($"ksh.convert({ chartPos }) effects disabled for { key }");
                        }
                    } break;

                    case "fx-l_se":
                    case "fx-r_se":
                    {
                        string chipFx = (string)setting.Value.Value;
                        float  volume = 1.0f;

                        if (chipFx.TrySplit(';', out string fxName, out string volStr))
                        {
                            chipFx = fxName;
                            if (volStr.Contains(';'))
                            {
                                volStr = volStr.Substring(0, volStr.IndexOf(';'));
                            }
                            volume = int.Parse(volStr) / 100.0f;
                        }

                        int i = key == "fx-l_se" ? 4 : 5;
                        chipHitSoundsVolume[i] = volume;
                        chipHitSounds[i]       = chipFx;
                    } break;

                    case "fx-l_param1":
                    {
                        Logger.Log($"ksh.convert({ chartPos }) skipping fx-l_param1.");
                    } break;

                    case "fx-r_param1":
                    {
                        Logger.Log($"ksh.convert({ chartPos }) skipping fx-r_param1.");
                    } break;

                    case "pfiltergain":
                    {
                        if (hasActiveEffects)
                        {
                            var laserGain = chart[NscLane.LaserEvent].Add <LaserFilterGainEvent>(chartPos);
                            laserGain.LaserIndex = LaserIndex.Both;
                            laserGain.Gain       = setting.Value.ToInt() / 100.0f;
                            Logger.Log($"ksh.convert({ chartPos }) set { key } { setting.Value }");
                        }
                        else
                        {
                            Logger.Log($"ksh.convert({ chartPos }) effects disabled for { key }");
                        }
                    } break;

                    case "filtertype":
                    {
                        if (hasActiveEffects)
                        {
                            var laserFilter = chart[NscLane.LaserEvent].Add <LaserFilterKindEvent>(chartPos);
                            laserFilter.LaserIndex = LaserIndex.Both;
                            laserFilter.Effect     = (setting.Value.Value as KshEffectRef)?.CreateEffectDef(ksh.FilterDefines);
                            Logger.Log($"ksh.convert({ chartPos }) set { key } { laserFilter.Effect?.GetType().Name ?? "nothing" }");
                        }
                        else
                        {
                            Logger.Log($"ksh.convert({ chartPos }) effects disabled for { key }");
                        }
                    }
                    break;

                    case "filter-l":     // NOTE(local): This is an extension, not originally supported in KSH. Used primarily for development purposes, but may also be exported to KSH should someone want to export back to that format.
                    case "filter-r":     // NOTE(local): This is an extension, not originally supported in KSH. Used primarily for development purposes, but may also be exported to KSH should someone want to export back to that format.
                    {
                        Logger.Log($"ksh.convert({ chartPos }) skipping { key }.");
                    }
                    break;

                    case "filter-l_gain":     // NOTE(local): This is an extension, not originally supported in KSH. Used primarily for development purposes, but may also be exported to KSH should someone want to export back to that format.
                    case "filter-r_gain":     // NOTE(local): This is an extension, not originally supported in KSH. Used primarily for development purposes, but may also be exported to KSH should someone want to export back to that format.
                    {
                        Logger.Log($"ksh.convert({ chartPos }) skipping { key }.");
                    }
                    break;

                    case "chokkakuvol":
                    {
                        var slamVoume = chart[NscLane.LaserEvent].Add <SlamVolumeEvent>(chartPos);
                        slamVoume.Volume = setting.Value.ToInt() / 100.0f;
                        Logger.Log($"ksh.convert({ chartPos }) set { key } { setting.Value }");
                    } break;

                    case "laserrange_l": { laserIsExtended[0] = true; } break;

                    case "laserrange_r": { laserIsExtended[1] = true; } break;

                    case "zoom_bottom":
                    {
                        var point = chart[NscLane.CameraZoom].Add <GraphPointEvent>(chartPos);
                        point.Value = setting.Value.ToInt() / 100.0f;
                        Logger.Log($"ksh.convert({ chartPos }) zoom { setting.Value }");
                    } break;

                    case "zoom_top":
                    {
                        var point = chart[NscLane.CameraPitch].Add <GraphPointEvent>(chartPos);
                        point.Value = setting.Value.ToInt() / 100.0f;
                        Logger.Log($"ksh.convert({ chartPos }) pitch { setting.Value }");
                    } break;

                    case "zoom_side":
                    {
                        var point = chart[NscLane.CameraOffset].Add <GraphPointEvent>(chartPos);
                        point.Value = setting.Value.ToInt() / 100.0f;
                        Logger.Log($"ksh.convert({ chartPos }) offset { setting.Value }");
                    } break;

                    case "split_0":
                    {
                        var point = chart[NscLane.Split0].Add <GraphPointEvent>(chartPos);
                        point.Value = setting.Value.ToInt() / 100.0f;
                        Logger.Log($"ksh.convert({ chartPos }) split 0 (l-a) { setting.Value }");
                    } break;

                    case "split_1":
                    {
                        var point = chart[NscLane.Split1].Add <GraphPointEvent>(chartPos);
                        point.Value = setting.Value.ToInt() / 100.0f;
                        Logger.Log($"ksh.convert({ chartPos }) split 1 (a-b) { setting.Value }");
                    } break;

                    case "center_split":
                    case "split_2":
                    {
                        var point = chart[NscLane.Split2].Add <GraphPointEvent>(chartPos);
                        point.Value = setting.Value.ToInt() / 100.0f;
                        Logger.Log($"ksh.convert({ chartPos }) split 2 (b-c) { setting.Value }");
                    } break;

                    case "split_3":
                    {
                        var point = chart[NscLane.Split3].Add <GraphPointEvent>(chartPos);
                        point.Value = setting.Value.ToInt() / 100.0f;
                        Logger.Log($"ksh.convert({ chartPos }) split 3 (c-d) { setting.Value }");
                    } break;

                    case "split_4":
                    {
                        var point = chart[NscLane.Split4].Add <GraphPointEvent>(chartPos);
                        point.Value = setting.Value.ToInt() / 100.0f;
                        Logger.Log($"ksh.convert({ chartPos }) split 4 (d-r) { setting.Value }");
                    } break;

                    case "roll":     // NOTE(local): This is an extension, not originally supported in KSH. Used primarily for development purposes, but may also be exported to KSH should someone want to export back to that format.
                    {
                        var point = chart[NscLane.CameraTilt].Add <GraphPointEvent>(chartPos);
                        point.Value = setting.Value.ToInt() / 360.0f;
                        Logger.Log($"ksh.convert({ chartPos }) custom manual tilt { setting.Value }");
                    } break;

                    case "tilt":
                    {
                        var laserApps = chart[NscLane.LaserEvent].Add <LaserApplicationEvent>(chartPos);

                        string v = setting.Value.ToString();
                        if (v.StartsWith("keep_"))
                        {
                            laserApps.Application = LaserApplication.Additive | LaserApplication.KeepMax;
                            v = v.Substring(5);
                        }

                        var laserParams = chart[NscLane.LaserEvent].Add <LaserParamsEvent>(chartPos);
                        laserParams.LaserIndex = LaserIndex.Both;

                        bool disableTilt = true;
                        switch (v)
                        {
                        default:
                        {
                            if (int.TryParse(v, out int manualValue))
                            {
                                disableTilt = false;

                                if (lastTiltEvent == null)
                                {
                                    var startPoint = chart[NscLane.CameraTilt].Add <GraphPointEvent>(chartPos);
                                    startPoint.Value = 0;
                                }

                                var point = chart[NscLane.CameraTilt].Add <GraphPointEvent>(chartPos);
                                point.Value = -manualValue * 14 / 360.0f;

                                lastTiltEvent = point;
                            }
                        } goto case "zero";

                        case "zero": laserParams.Params.Function = LaserFunction.Zero; break;

                        case "normal": laserParams.Params.Scale = LaserScale.Normal; break;

                        case "bigger": laserParams.Params.Scale = LaserScale.Bigger; break;

                        case "biggest": laserParams.Params.Scale = LaserScale.Biggest; break;
                        }

                        if (disableTilt && lastTiltEvent != null)
                        {
                        }
                    } break;

                    case "fx_sample":
                    {
                        Logger.Log($"ksh.convert({ chartPos }) skipping fx_sample.");
                    } break;

                    case "stop":
                    {
                        ControlPoint cp = chart.ControlPoints.GetOrCreate(chartPos, true);
                        // TODO(local): this breaks when there's another control point between here and there
                        chart.ControlPoints.GetOrCreate(chartPos + int.Parse(setting.Value.ToString()) / 192.0, true);

                        cp.StopChart = true;
                        lastCp       = cp;

                        Logger.Log($"ksh.convert({ chartPos }) stop { int.Parse(setting.Value.ToString()) / 192.0 }");
                    } break;

                    case "lane_toggle":
                    {
                        Logger.Log($"ksh.convert({ chartPos }) skipping lane_toggle.");
                    } break;
                    }
                }

                for (int b = 0; b < 6; b++)
                {
                    bool isFx = b >= 4;

                    var data = isFx ? tick.Fx[b - 4] : tick.Bt[b];

                    void CreateHold(tick_t endPos)
                    {
                        var state = buttonStates[b];

                        var startPos = state.StartPosition;
                        var button   = chart[(HybridLabel)b].Add <ButtonEntity>(startPos, endPos - startPos);
                    }

                    switch (data.State)
                    {
                    case KshButtonState.Off:
                    {
                        if (buttonStates[b] != null)
                        {
                            CreateHold(chartPos);
                        }
                        buttonStates[b] = null;
                    } break;

                    case KshButtonState.Chip:
                    case KshButtonState.ChipSample:
                    {
                        var chip = chart[(HybridLabel)b].Add <ButtonEntity>(chartPos);
                        chip.Sample       = chipHitSounds[b];
                        chip.SampleVolume = chipHitSoundsVolume[b];
                    } break;

                    case KshButtonState.Hold:
                    {
                        if (buttonStates[b] == null)
                        {
                            buttonStates[b] = new TempButtonState(chartPos);
                        }
                    } break;
                    }
                }

                for (int l = 0; l < 2; l++)
                {
                    var data  = tick.Laser[l];
                    var state = data.State;

                    tick_t CreateSegment(tick_t endPos, float endAlpha)
                    {
                        var   startPos   = laserStates[l].StartPosition;
                        float startAlpha = laserStates[l].StartAlpha;

                        var duration = endPos - startPos;

                        //if (duration <= tick_t.FromFraction(1, 32 * lastCp.BeatCount / lastCp.BeatKind))
                        if (laserStates[l].HiResTickCount <= 6 && startAlpha != endAlpha)
                        {
                            duration = 0;

                            if (laserStates[l].PreviousSlamDuration != 0)
                            {
                                var cDuration = laserStates[l].PreviousSlamDuration;

                                var connector = chart[(HybridLabel)(l + 6)].Add <AnalogEntity>(startPos, cDuration);
                                connector.InitialValue  = startAlpha;
                                connector.FinalValue    = startAlpha;
                                connector.RangeExtended = laserIsExtended[l];

                                startPos += cDuration;
                            }
                        }

                        var analog = chart[(HybridLabel)(l + 6)].Add <AnalogEntity>(startPos, duration);

                        analog.InitialValue  = startAlpha;
                        analog.FinalValue    = endAlpha;
                        analog.RangeExtended = laserIsExtended[l];
                        analog.Shape         = laserStates[l].Shape;
                        analog.CurveA        = laserStates[l].CurveA;
                        analog.CurveB        = laserStates[l].CurveB;

                        return(startPos + duration);
                    }

                    switch (state)
                    {
                    case KshLaserState.Inactive:
                    {
                        if (laserStates[l] != null)
                        {
                            laserStates[l]     = null;
                            laserIsExtended[l] = false;
                        }
                    } break;

                    case KshLaserState.Lerp:
                    {
                        laserStates[l].HiResTickCount += (192 * lastCp.BeatCount / lastCp.BeatKind) / tickRef.MaxIndex;
                    } break;

                    case KshLaserState.Position:
                    {
                        var alpha    = data.Position;
                        var startPos = chartPos;

                        tick_t prevSlamDuration = 0;
                        if (laserStates[l] != null)
                        {
                            startPos = CreateSegment(chartPos, alpha.Alpha);
                            if (startPos != chartPos)
                            {
                                prevSlamDuration = chartPos - startPos;
                            }
                        }

                        var ls = laserStates[l] = new TempLaserState(startPos, lastCp)
                        {
                            StartAlpha           = alpha.Alpha,
                            HiResTickCount       = (192 * lastCp.BeatCount / lastCp.BeatKind) / tickRef.MaxIndex,
                            PreviousSlamDuration = prevSlamDuration,
                            CurveResolution      = 0,
                        };

                        for (int i = tick.Comments.Count - 1; i >= 0; i--)
                        {
                            string c = tick.Comments[i];
                            if (!c.StartsWith("LaserShape "))
                            {
                                continue;
                            }
                            c = c.Substring("LaserShape ".Length).Trim();

                            if (c.StartsWith("ThreePoint"))
                            {
                                float a = 0.5f, b = 0.5f;
                                if (c != "ThreePoint" && c.TrySplit(' ', out string tp, out string sa, out string sb))
                                {
                                    float.TryParse(sa, out a);
                                    float.TryParse(sb, out b);
                                }

                                ls.Shape  = CurveShape.ThreePoint;
                                ls.CurveA = a;
                                ls.CurveB = b;
                            }
                            else if (c.StartsWith("Cosine"))
                            {
                                float a = 0.0f;
                                if (c != "Cosine" && c.TrySplit(' ', out string tp, out string sa))
                                {
                                    float.TryParse(sa, out a);
                                }

                                ls.Shape  = CurveShape.Cosine;
                                ls.CurveA = a;
                            }
                            else
                            {
                                continue;
                            }
                            break;
                        }
                    } break;
                    }
                }

                switch (tick.Add.Kind)
                {
                case KshAddKind.None: break;

                case KshAddKind.Spin:
                {
                    tick_t duration = tick_t.FromFraction(tick.Add.Duration * 2, 192);
                    var    spin     = chart[NscLane.HighwayEvent].Add <SpinImpulseEvent>(chartPos, duration);
                    spin.Direction = (AngularDirection)tick.Add.Direction;
                } break;

                case KshAddKind.Swing:
                {
                    tick_t duration = tick_t.FromFraction(tick.Add.Duration * 2, 192);
                    var    swing    = chart[NscLane.HighwayEvent].Add <SwingImpulseEvent>(chartPos, duration);
                    swing.Direction = (AngularDirection)tick.Add.Direction;
                    swing.Amplitude = tick.Add.Amplitude * 70 / 100.0f;
                } break;

                case KshAddKind.Wobble:
                {
                    tick_t duration = tick_t.FromFraction(tick.Add.Duration, 192);
                    var    wobble   = chart[NscLane.HighwayEvent].Add <WobbleImpulseEvent>(chartPos, duration);
                    wobble.Direction = (LinearDirection)tick.Add.Direction;
                    wobble.Amplitude = tick.Add.Amplitude / 250.0f;
                    wobble.Decay     = (Decay)tick.Add.Decay;
                    wobble.Frequency = tick.Add.Frequency;
                } break;
                }
            }

            Logger.Log($"ksh.convert end");
            return(chart);
        }