Exemplo n.º 1
0
        private void UpdateLaserState(int index, BeatmapKsh.Tick tick)
        {
            Laser          laser;
            TempLaserState state;

            if (laserStates[index] == null)
            {
                state = laserStates[index] = new TempLaserState();

                // Create laser root
                var root = new LaserRoot();
                root.IsExtended = laserExtended[index];
                root.Index      = index;
                laser           = root;

                state.Root       = root.Root = new ObjectReference(root, currentMeasure);
                state.LastObject = state.Root;
            }
            else
            {
                state = laserStates[index];

                // Create laser point
                laser          = new Laser();
                laser.Previous = state.LastObject;
                laser.Root     = state.Root;

                var myRef = new ObjectReference(laser, currentMeasure);

                // Calculate distance to last
                var lastObject = (state.LastObject.Object as Laser);

                lastObject.Next  = myRef;
                state.LastObject = myRef;
            }

            laser.Position           = currentPosition;
            laser.HorizontalPosition = tick.LaserAsFloat(index);
            if (laser.IsExtended)
            {
                laser.HorizontalPosition *= 2.0f;
                laser.HorizontalPosition -= 0.5f;
            }

            // Decide to create slam instead?
            var  previousLaser        = laser.Previous?.Object as Laser;
            bool createInstantSegment = false;

            if (previousLaser != null)
            {
                double laserSlamThreshold = currentTimingPoint.BeatDuration / 7.0;
                double duration           = previousLaser.Next.AbsolutePosition - laser.Previous.AbsolutePosition;
                createInstantSegment = duration <= laserSlamThreshold &&
                                       previousLaser.HorizontalPosition != laser.HorizontalPosition;
            }
            if (createInstantSegment)
            {
                laser.Previous.Measure.Objects.Add(laser);
                laser.Position = laser.Previous.Object.Position;
            }
            else
            {
                // Create normal control point
                currentMeasure.Objects.Add(laser);
            }

            // Reset tick and last measure on laser state
            state.NumTicks          = 0;
            state.LastSourceMeasure = sourceMeasure;
        }
Exemplo n.º 2
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);
        }
    }
Exemplo n.º 3
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);
        }