Esempio n. 1
0
        public static bool Save(Project originalProject, string filename, int[] songIds)
        {
            var project = originalProject.Clone();

            project.RemoveAllSongsBut(songIds);

            ConvertPitchEnvelopes(project);
            var envelopes = MergeIdenticalEnvelopes(project);

            var lines = new List <string>();

            lines.Add("# FamiTracker text export 0.4.2");
            lines.Add("");

            lines.Add("# Song information");
            lines.Add("TITLE           \"" + project.Name + "\"");
            lines.Add("AUTHOR          \"" + project.Author + "\"");
            lines.Add("COPYRIGHT       \"" + project.Copyright + "\"");
            lines.Add("");

            lines.Add("# Global settings");
            lines.Add("MACHINE         0");
            lines.Add("FRAMERATE       0");
            lines.Add("EXPANSION       " + project.ExpansionAudio);
            lines.Add("VIBRATO         1");
            lines.Add("SPLIT           21");
            lines.Add("");

            lines.Add("# Macros");
            for (int i = 0; i < Envelope.Max; i++)
            {
                var envArray = envelopes[Project.ExpansionNone, i];
                for (int j = 0; j < envArray.Length; j++)
                {
                    var env = envArray[j];
                    lines.Add($"MACRO{i,8} {j,4} {env.Loop,4} {(env.Release >= 0 ? env.Release - 1 : -1),4}   0 : {string.Join(" ", env.Values.Take(env.Length))}");
                }
            }
            lines.Add($"MACRO{4,8} {0,4} {-1}   -1    0 : 0");
            lines.Add($"MACRO{4,8} {1,4} {-1}   -1    0 : 1");
            lines.Add($"MACRO{4,8} {2,4} {-1}   -1    0 : 2");
            lines.Add($"MACRO{4,8} {3,4} {-1}   -1    0 : 3");

            if (project.ExpansionAudio == Project.ExpansionVrc6)
            {
                for (int i = 0; i < Envelope.Max; i++)
                {
                    var envArray = envelopes[Project.ExpansionVrc6, i];
                    for (int j = 0; j < envArray.Length; j++)
                    {
                        var env = envArray[j];
                        lines.Add($"MACROVRC6{i,8} {j,4} {env.Loop,4} {(env.Release >= 0 ? env.Release - 1 : -1),4}   0 : {string.Join(" ", env.Values.Take(env.Length))}");
                    }
                }

                lines.Add($"MACROVRC6{4,8} {0,4} {-1}   -1    0 : 0");
                lines.Add($"MACROVRC6{4,8} {1,4} {-1}   -1    0 : 1");
                lines.Add($"MACROVRC6{4,8} {2,4} {-1}   -1    0 : 2");
                lines.Add($"MACROVRC6{4,8} {3,4} {-1}   -1    0 : 3");
                lines.Add($"MACROVRC6{4,8} {4,4} {-1}   -1    0 : 4");
                lines.Add($"MACROVRC6{4,8} {5,4} {-1}   -1    0 : 5");
                lines.Add($"MACROVRC6{4,8} {6,4} {-1}   -1    0 : 6");
                lines.Add($"MACROVRC6{4,8} {7,4} {-1}   -1    0 : 7");
            }

            lines.Add("");

            if (project.UsesSamples)
            {
                lines.Add("# DPCM samples");
                for (int i = 0; i < project.Samples.Count; i++)
                {
                    var sample = project.Samples[i];
                    lines.Add($"DPCMDEF{i,4}{sample.Data.Length,6} \"{sample.Name}\"");
                    lines.Add($"DPCM : {String.Join(" ", sample.Data.Select(x => $"{x:X2}"))}");
                }
                lines.Add("");
            }

            lines.Add("# Instruments");
            for (int i = 0; i < project.Instruments.Count; i++)
            {
                var instrument = project.Instruments[i];

                var expIdx    = instrument.IsExpansionInstrument ? 1 : 0;
                int volEnvIdx = instrument.Envelopes[Envelope.Volume].Length > 0 ? Array.IndexOf(envelopes[expIdx, Envelope.Volume], instrument.Envelopes[Envelope.Volume])   : -1;
                int arpEnvIdx = instrument.Envelopes[Envelope.Arpeggio].Length > 0 ? Array.IndexOf(envelopes[expIdx, Envelope.Arpeggio], instrument.Envelopes[Envelope.Arpeggio]) : -1;
                int pitEnvIdx = instrument.Envelopes[Envelope.Pitch].Length > 0 ? Array.IndexOf(envelopes[expIdx, Envelope.Pitch], instrument.Envelopes[Envelope.Pitch])    : -1;

                if (instrument.ExpansionType == Project.ExpansionNone)
                {
                    lines.Add($"INST2A03{i,4}{volEnvIdx,6}{arpEnvIdx,4}{pitEnvIdx,4}{-1,4}{instrument.DutyCycle,4} \"{instrument.Name}\"");
                }
                else if (instrument.ExpansionType == Project.ExpansionVrc6)
                {
                    lines.Add($"INSTVRC6{i,4}{volEnvIdx,6}{arpEnvIdx,4}{pitEnvIdx,4}{-1,4}{instrument.DutyCycle,4} \"{instrument.Name}\"");
                }
            }

            if (project.UsesSamples)
            {
                lines.Add($"INST2A03{project.Instruments.Count,4}{-1,6}{-1,4}{-1,4}{-1,4}{-1,4} \"DPCM\"");

                for (int i = 0; i < project.SamplesMapping.Length; i++)
                {
                    var mapping = project.SamplesMapping[i];

                    if (mapping != null && mapping.Sample != null)
                    {
                        int note     = i + Note.DPCMNoteMin;
                        var octave   = (note - 1) / 12;
                        var semitone = (note - 1) % 12;
                        var idx      = project.Samples.IndexOf(mapping.Sample);
                        var loop     = mapping.Loop ? 1 : 0;

                        lines.Add($"KEYDPCM{project.Instruments.Count,4}{octave,4}{semitone,4}{idx,6}{mapping.Pitch,4}{loop,4}{0,6}{-1,4}");
                    }
                }
            }
            lines.Add("");

            lines.Add("# Tracks");
            for (int i = 0; i < project.Songs.Count; i++)
            {
                var song = project.Songs[i];

                song.CleanupUnusedPatterns();
                CreateMissingPatterns(song);

                // Find all the places where we need to turn of 1xx/2xx/3xx after we are done.
                //var portamentoTransitions = new Dictionary<Pattern, List<int>>();
                //var slideTransitions = new Dictionary<Pattern, List<int>>();
                //FindSlideNoteTransitions(song, portamentoTransitions, slideTransitions);

                lines.Add($"TRACK{song.PatternLength,4}{song.Speed,4}{song.Tempo,4} \"{song.Name}\"");
                lines.Add($"COLUMNS : {string.Join(" ", Enumerable.Repeat(3, song.Channels.Length))}");
                lines.Add("");

                for (int j = 0; j < song.Length; j++)
                {
                    var line = $"ORDER {j:X2} :";

                    for (int k = 0; k < song.Channels.Length; k++)
                    {
                        line += $" {song.Channels[k].Patterns.IndexOf(song.Channels[k].PatternInstances[j]):X2}";
                    }

                    lines.Add(line);
                }
                lines.Add("");

                int maxPatternCount = -1;
                foreach (var channel in song.Channels)
                {
                    maxPatternCount = Math.Max(maxPatternCount, channel.Patterns.Count);
                }

                var patternRows = new Dictionary <Pattern, List <string> >();
                for (int c = 0; c < song.Channels.Length; c++)
                {
                    var channel         = song.Channels[c];
                    var prevNoteValue   = Note.NoteInvalid;
                    var prevSlideEffect = '\0';

                    for (int p = 0; p < song.Length; p++)
                    {
                        var pattern = channel.PatternInstances[p];

                        if (patternRows.ContainsKey(pattern))
                        {
                            continue;
                        }

                        var patternLines = new List <string>();

                        for (int n = 0; n < song.PatternLength; n++)
                        {
                            var note             = pattern.Notes[n];
                            var noteString       = GetFamiTrackerNoteName(c, note);
                            var volumeString     = note.HasVolume ? note.Volume.ToString("X") : ".";
                            var instrumentString = note.IsValid && !note.IsStop ? (note.Instrument == null ? project.Instruments.Count : project.Instruments.IndexOf(note.Instrument)).ToString("X2") : "..";
                            var effectString     = "";
                            var noAttack         = !note.HasAttack && prevNoteValue == note.Value && (prevSlideEffect == '\0' || prevSlideEffect == 'Q' || prevSlideEffect == '3');

                            if (note.IsSlideNote && note.IsMusical)
                            {
                                // TODO: PAL.
                                var noteTable = NesApu.GetNoteTableForChannelType(channel.Type, false);
                                channel.ComputeSlideNoteParams(p, n, noteTable, out _, out int stepSize, out _);

                                var absNoteDelta = Math.Abs(note.Value - note.SlideNoteTarget);

                                // See if we can use Qxy/Rxy (slide up/down y semitones, at speed x), this is preferable.
                                if (absNoteDelta < 16)
                                {
                                    if (prevSlideEffect == '1' || prevSlideEffect == '2' || prevSlideEffect == '3')
                                    {
                                        effectString += $" {prevSlideEffect}00";
                                    }

                                    // FamiTracker use 2x + 1, find the number that is just above our speed.
                                    var speed = 0;
                                    for (int x = 14; x >= 0; x--)
                                    {
                                        if ((2 * x + 1) < Math.Abs(stepSize / 2.0f))
                                        {
                                            speed = x + 1;
                                            break;
                                        }
                                    }

                                    if (note.SlideNoteTarget > note.Value)
                                    {
                                        effectString += $" Q{speed:X1}{absNoteDelta:X1}";
                                    }
                                    else
                                    {
                                        effectString += $" R{speed:X1}{absNoteDelta:X1}";
                                    }

                                    prevSlideEffect = 'Q';
                                }
                                else
                                {
                                    // We have one bit of fraction. FramiTracker does not.
                                    var ceilStepSize = Utils.SignedCeil(stepSize / 2.0f);

                                    // If the previous note matched too, we can use 3xx (auto-portamento).
                                    if (prevNoteValue == note.Value)
                                    {
                                        if (prevSlideEffect == '1' || prevSlideEffect == '2')
                                        {
                                            effectString += $" 100";
                                        }

                                        noteString      = GetFamiTrackerNoteName(c, new Note(note.SlideNoteTarget));
                                        effectString   += $" 3{Math.Abs(ceilStepSize):X2}";
                                        prevSlideEffect = '3';
                                        noAttack        = false; // Need to force attack when starting auto-portamento unfortunately.
                                    }
                                    else
                                    {
                                        // We have one bit of fraction. FramiTracker does not.
                                        var floorStepSize = Utils.SignedFloor(stepSize / 2.0f);

                                        if (prevSlideEffect == '3')
                                        {
                                            effectString += $" 300";
                                        }

                                        if (stepSize > 0)
                                        {
                                            effectString   += $" 2{ floorStepSize:X2}";
                                            prevSlideEffect = '2';
                                        }
                                        else if (stepSize < 0)
                                        {
                                            effectString   += $" 1{-floorStepSize:X2}";
                                            prevSlideEffect = '1';
                                        }
                                    }
                                }
                            }
                            else if ((note.IsMusical || note.IsStop) && prevSlideEffect != '\0')
                            {
                                if (prevSlideEffect == '1' || prevSlideEffect == '2' || prevSlideEffect == '3')
                                {
                                    effectString += $" {prevSlideEffect}00";
                                }

                                prevSlideEffect = '\0';
                            }

                            if (note.HasJump)
                            {
                                effectString += $" B{note.Jump:X2}";
                            }
                            if (note.HasSkip)
                            {
                                effectString += $" D{note.Skip:X2}";
                            }
                            if (note.HasSpeed)
                            {
                                effectString += $" F{note.Speed:X2}";
                            }
                            if (note.HasVibrato)
                            {
                                effectString += $" 4{VibratoSpeedExportLookup[note.VibratoSpeed]:X1}{note.VibratoDepth:X1}";
                            }

                            while (effectString.Length < 12)
                            {
                                effectString += " ...";
                            }

                            if (noAttack)
                            {
                                noteString       = "...";
                                instrumentString = "..";
                            }

                            var line = $" : {noteString} {instrumentString} {volumeString}{effectString}";

                            if (note.IsMusical || note.IsStop)
                            {
                                prevNoteValue = note.IsSlideNote ? note.SlideNoteTarget : note.Value;
                            }

                            patternLines.Add(line);
                        }

                        patternRows[pattern] = patternLines;
                    }
                }

                for (int j = 0; j < maxPatternCount; j++)
                {
                    lines.Add($"PATTERN {j:X2}");

                    for (int p = 0; p < song.PatternLength; p++)
                    {
                        var line = $"ROW {p:X2}";
                        for (int c = 0; c < song.Channels.Length; c++)
                        {
                            var channel = song.Channels[c];

                            if (j >= channel.Patterns.Count)
                            {
                                line += " : ... .. . ... ... ...";
                            }
                            else
                            {
                                line += patternRows[channel.Patterns[j]][p];
                            }
                        }

                        lines.Add(line);
                    }

                    lines.Add("");
                }
            }

            File.WriteAllLines(filename, lines);

            return(true);
        }
Esempio n. 2
0
        public bool Save(Project originalProject, string filename, int[] songIds)
        {
            var project = originalProject.DeepClone();

            project.RemoveAllSongsBut(songIds);

            if (project.UsesFamiStudioTempo)
            {
                project.ConvertToFamiTrackerTempo(false);
            }

            ConvertPitchEnvelopes(project);
            var envelopes = MergeIdenticalEnvelopes(project);

            var lines = new List <string>();

            lines.Add("# FamiTracker text export 0.4.2");
            lines.Add("");

            lines.Add("# Song information");
            lines.Add("TITLE           \"" + project.Name + "\"");
            lines.Add("AUTHOR          \"" + project.Author + "\"");
            lines.Add("COPYRIGHT       \"" + project.Copyright + "\"");
            lines.Add("");

            lines.Add("# Song comment");
            lines.Add("COMMENT         \"\"");

            lines.Add("# Global settings");
            lines.Add("MACHINE         0");
            lines.Add("FRAMERATE       0");
            lines.Add("EXPANSION       " + (project.ExpansionAudio != Project.ExpansionNone ? (1 << (project.ExpansionAudio - 1)) : 0));
            lines.Add("VIBRATO         1");
            lines.Add("SPLIT           32");
            lines.Add("");

            var realNumExpansionChannels = project.ExpansionNumChannels;

            if (project.ExpansionAudio == Project.ExpansionN163)
            {
                lines.Add("# Namco 163 global settings");
                lines.Add($"N163CHANNELS    {project.ExpansionNumChannels}");
                lines.Add("");

                // The text format always export all 8 channels, even if there are less.
                project.SetExpansionAudio(Project.ExpansionN163, 8);
            }

            lines.Add("# Macros");
            for (int i = 0; i < Envelope.RegularCount; i++)
            {
                var envArray = envelopes[Project.ExpansionNone, i];
                for (int j = 0; j < envArray.Length; j++)
                {
                    var env = envArray[j];
                    lines.Add($"MACRO{ReverseEnvelopeTypeLookup[i],8} {j,4} {env.Loop,4} {(env.Release >= 0 ? env.Release - 1 : -1),4}   0 : {string.Join(" ", env.Values.Take(env.Length))}");
                }
            }

            if (project.ExpansionAudio == Project.ExpansionVrc6 ||
                project.ExpansionAudio == Project.ExpansionN163)
            {
                var suffix = project.ExpansionAudio == Project.ExpansionVrc6 ? "VRC6" : "N163";

                for (int i = 0; i < Envelope.RegularCount; i++)
                {
                    var envArray = envelopes[1, i];
                    for (int j = 0; j < envArray.Length; j++)
                    {
                        var env = envArray[j];
                        lines.Add($"MACRO{suffix}{i,8} {j,4} {env.Loop,4} {(env.Release >= 0 ? env.Release - 1 : -1),4}   0 : {string.Join(" ", env.Values.Take(env.Length))}");
                    }
                }
            }

            lines.Add("");

            if (project.UsesSamples)
            {
                lines.Add("# DPCM samples");
                for (int i = 0; i < project.Samples.Count; i++)
                {
                    var sample = project.Samples[i];
                    lines.Add($"DPCMDEF{i,4}{sample.Data.Length,6} \"{sample.Name}\"");
                    lines.Add($"DPCM : {String.Join(" ", sample.Data.Select(x => $"{x:X2}"))}");
                }
                lines.Add("");
            }

            lines.Add("# Instruments");
            for (int i = 0; i < project.Instruments.Count; i++)
            {
                var instrument = project.Instruments[i];

                var volEnv = instrument.Envelopes[Envelope.Volume];
                var arpEnv = instrument.Envelopes[Envelope.Arpeggio];
                var pitEnv = instrument.Envelopes[Envelope.Pitch];
                var dutEnv = instrument.Envelopes[Envelope.DutyCycle];

                var expIdx    = instrument.IsExpansionInstrument ? 1 : 0;
                int volEnvIdx = volEnv != null && volEnv.Length > 0 ? Array.IndexOf(envelopes[expIdx, Envelope.Volume], instrument.Envelopes[Envelope.Volume])    : -1;
                int arpEnvIdx = arpEnv != null && arpEnv.Length > 0 ? Array.IndexOf(envelopes[expIdx, Envelope.Arpeggio], instrument.Envelopes[Envelope.Arpeggio])  : -1;
                int pitEnvIdx = pitEnv != null && pitEnv.Length > 0 ? Array.IndexOf(envelopes[expIdx, Envelope.Pitch], instrument.Envelopes[Envelope.Pitch])     : -1;
                int dutEnvIdx = dutEnv != null && dutEnv.Length > 0 ? Array.IndexOf(envelopes[expIdx, Envelope.DutyCycle], instrument.Envelopes[Envelope.DutyCycle]) : -1;

                if (instrument.ExpansionType == Project.ExpansionNone)
                {
                    lines.Add($"INST2A03{i,4}{volEnvIdx,6}{arpEnvIdx,4}{pitEnvIdx,4}{-1,4}{dutEnvIdx,4} \"{instrument.Name}\"");
                }
                else if (instrument.ExpansionType == Project.ExpansionVrc6)
                {
                    lines.Add($"INSTVRC6{i,4}{volEnvIdx,6}{arpEnvIdx,4}{pitEnvIdx,4}{-1,4}{dutEnvIdx,4} \"{instrument.Name}\"");
                }
                else if (instrument.ExpansionType == Project.ExpansionVrc7)
                {
                    lines.Add($"INSTVRC7{i,4}{instrument.Vrc7Patch,4} {String.Join(" ", instrument.Vrc7PatchRegs.Select(x => $"{x:X2}"))} \"{instrument.Name}\"");
                }
                else if (instrument.ExpansionType == Project.ExpansionN163)
                {
                    lines.Add($"INSTN163{i,4}{volEnvIdx,6}{arpEnvIdx,4}{pitEnvIdx,4}{-1,4}{dutEnvIdx,4}{instrument.N163WaveSize,4}{instrument.N163WavePos,4}{1,4} \"{instrument.Name}\"");

                    var wavEnv = instrument.Envelopes[Envelope.N163Waveform];
                    lines.Add($"N163WAVE{i,4}{0,6} : {string.Join(" ", wavEnv.Values.Take(wavEnv.Length))}");
                }
                else if (instrument.ExpansionType == Project.ExpansionFds)
                {
                    lines.Add($"INSTFDS{i,5}{1,6}{instrument.FdsModSpeed,4}{instrument.FdsModDepth,4}{instrument.FdsModDelay,4} \"{instrument.Name}\"");

                    var wavEnv = instrument.Envelopes[Envelope.FdsWaveform];
                    lines.Add($"FDSWAVE{i,5} : {string.Join(" ", wavEnv.Values.Take(wavEnv.Length))}");
                    var modEnv = instrument.Envelopes[Envelope.FdsModulation].BuildFdsModulationTable();
                    lines.Add($"FDSMOD{i,6} : {string.Join(" ", modEnv.Take(modEnv.Length))}");

                    for (int j = 0; j <= Envelope.Pitch; j++)
                    {
                        var env = instrument.Envelopes[j];
                        if (!env.IsEmpty)
                        {
                            lines.Add($"FDSMACRO{i,4} {j,5} {env.Loop,4} {(env.Release >= 0 ? env.Release - 1 : -1),4}   0 : {string.Join(" ", env.Values.Take(env.Length))}");
                        }
                    }
                }
            }

            if (project.UsesSamples)
            {
                lines.Add($"INST2A03{project.Instruments.Count,4}{-1,6}{-1,4}{-1,4}{-1,4}{-1,4} \"DPCM\"");

                for (int i = 0; i < project.SamplesMapping.Length; i++)
                {
                    var mapping = project.SamplesMapping[i];

                    if (mapping != null && mapping.Sample != null)
                    {
                        int note     = i + Note.DPCMNoteMin;
                        var octave   = (note - 1) / 12;
                        var semitone = (note - 1) % 12;
                        var idx      = project.Samples.IndexOf(mapping.Sample);
                        var loop     = mapping.Loop ? 1 : 0;

                        lines.Add($"KEYDPCM{project.Instruments.Count,4}{octave,4}{semitone,4}{idx,6}{mapping.Pitch,4}{loop,4}{0,6}{-1,4}");
                    }
                }
            }
            lines.Add("");

            lines.Add("# Tracks");
            for (int i = 0; i < project.Songs.Count; i++)
            {
                var song = project.Songs[i];

                TruncateLongPatterns(song);
                CreateMissingPatterns(song);
                song.CleanupUnusedPatterns();
                song.DuplicateInstancesWithDifferentLengths();

                lines.Add($"TRACK{song.PatternLength,4}{song.FamitrackerSpeed,4}{song.FamitrackerTempo,4} \"{song.Name}\"");
                lines.Add($"COLUMNS : {string.Join(" ", Enumerable.Repeat(3, song.Channels.Length))}");
                lines.Add("");

                for (int j = 0; j < song.Length; j++)
                {
                    var line = $"ORDER {j:X2} :";

                    for (int k = 0; k < song.Channels.Length; k++)
                    {
                        line += $" {song.Channels[k].Patterns.IndexOf(song.Channels[k].PatternInstances[j]):X2}";
                    }

                    lines.Add(line);
                }
                lines.Add("");

                int maxPatternCount = -1;
                foreach (var channel in song.Channels)
                {
                    maxPatternCount = Math.Max(maxPatternCount, channel.Patterns.Count);
                }

                var patternRows = new Dictionary <Pattern, List <string> >();
                for (int c = 0; c < song.Channels.Length; c++)
                {
                    var channel         = song.Channels[c];
                    var prevNoteValue   = Note.NoteInvalid;
                    var prevInstrument  = (Instrument)null;
                    var prevSlideEffect = Effect_None;

                    for (int p = 0; p < song.Length; p++)
                    {
                        var pattern    = channel.PatternInstances[p];
                        var patternLen = song.GetPatternLength(p);

                        if (patternRows.ContainsKey(pattern))
                        {
                            continue;
                        }

                        var patternLines = new List <string>();

                        for (var it = pattern.GetNoteIterator(0, song.PatternLength); !it.Done; it.Next())
                        {
                            var time = it.CurrentTime;
                            var note = it.CurrentNote;

                            // Keeps the code a lot simpler.
                            if (note == null)
                            {
                                note = Note.EmptyNote;
                            }

                            var line = " : ... .. . ... ... ...";

                            var noteString       = GetFamiTrackerNoteName(c, note);
                            var volumeString     = note.HasVolume ? note.Volume.ToString("X") : ".";
                            var instrumentString = note.IsValid && !note.IsStop ? (note.Instrument == null ? project.Instruments.Count : project.Instruments.IndexOf(note.Instrument)).ToString("X2") : "..";
                            var effectString     = "";
                            var noAttack         = !note.HasAttack && prevNoteValue == note.Value && (prevSlideEffect == Effect_None || prevSlideEffect == Effect_SlideUp || prevSlideEffect == Effect_Portamento);

                            if (note.IsSlideNote && note.IsMusical)
                            {
                                var noteTable = NesApu.GetNoteTableForChannelType(channel.Type, false, realNumExpansionChannels);

                                var noteValue   = note.Value;
                                var slideTarget = note.SlideNoteTarget;

                                // FamiTracker only has 12-pitches and doesnt change the octave when doing
                                // slides. This helps make the slides more compatible, but its not great.
                                if (channel.IsVrc7FmChannel)
                                {
                                    while (noteValue >= 12 && slideTarget >= 12)
                                    {
                                        noteValue   -= 12;
                                        slideTarget -= 12;
                                    }
                                }

                                // TODO: We use the initial FamiTracker speed here, this is wrong, it might have changed. Also we assume NTSC here.
                                var stepSizeFloat = channel.ComputeRawSlideNoteParams(noteValue, slideTarget, p, time, song.FamitrackerSpeed, Song.NativeTempoNTSC, noteTable);

                                if (channel.IsN163WaveChannel)
                                {
                                    stepSizeFloat /= 4.0f;
                                }

                                // Undo any kind of shifting we had done. This will kill the 1-bit of fraction we have on most channel.
                                var absNoteDelta = Math.Abs(note.Value - note.SlideNoteTarget);

                                // See if we can use Qxy/Rxy (slide up/down y semitones, at speed x), this is preferable.
                                if (absNoteDelta < 16)
                                {
                                    if (prevSlideEffect == Effect_PortaUp ||
                                        prevSlideEffect == Effect_PortaDown ||
                                        prevSlideEffect == Effect_Portamento)
                                    {
                                        effectString += $" {EffectToTextLookup[prevSlideEffect]}00";
                                    }

                                    // FamiTracker use 2x + 1, find the number that is just above our speed.
                                    var speed = 0;
                                    for (int x = 14; x >= 0; x--)
                                    {
                                        if ((2 * x + 1) < Math.Abs(stepSizeFloat))
                                        {
                                            speed = x + 1;
                                            break;
                                        }
                                    }

                                    if (note.SlideNoteTarget > note.Value)
                                    {
                                        effectString += $" Q{speed:X1}{absNoteDelta:X1}";
                                    }
                                    else
                                    {
                                        effectString += $" R{speed:X1}{absNoteDelta:X1}";
                                    }

                                    prevSlideEffect = Effect_SlideUp;
                                }
                                else
                                {
                                    // We have one bit of fraction. FramiTracker does not.
                                    var ceilStepSize = Utils.SignedCeil(stepSizeFloat);

                                    // If the previous note matched too, we can use 3xx (auto-portamento).
                                    // Avoid using portamento on instrument with relative pitch envelopes, their previous pitch isnt reliable.
                                    if (prevNoteValue == note.Value && (prevInstrument == null || prevInstrument.Envelopes[Envelope.Pitch].IsEmpty || !prevInstrument.Envelopes[Envelope.Pitch].Relative))
                                    {
                                        if (prevSlideEffect == Effect_PortaUp ||
                                            prevSlideEffect == Effect_PortaDown)
                                        {
                                            effectString += $" 100";
                                        }

                                        noteString      = GetFamiTrackerNoteName(c, new Note(note.SlideNoteTarget));
                                        effectString   += $" 3{Math.Min(0xff, Math.Abs(ceilStepSize)):X2}";
                                        prevSlideEffect = Effect_Portamento;
                                        noAttack        = false; // Need to force attack when starting auto-portamento unfortunately.
                                    }
                                    else
                                    {
                                        // Inverted channels.
                                        if (channel.IsFdsWaveChannel || channel.IsN163WaveChannel)
                                        {
                                            stepSizeFloat = -stepSizeFloat;
                                        }

                                        var absFloorStepSize = Math.Abs(Utils.SignedFloor(stepSizeFloat));

                                        if (prevSlideEffect == Effect_Portamento)
                                        {
                                            effectString += $" 300";
                                        }

                                        if (note.SlideNoteTarget > note.Value)
                                        {
                                            effectString   += $" 1{Math.Min(0xff, absFloorStepSize):X2}";
                                            prevSlideEffect = Effect_PortaUp;
                                        }
                                        else if (note.SlideNoteTarget < note.Value)
                                        {
                                            effectString   += $" 2{Math.Min(0xff, absFloorStepSize):X2}";
                                            prevSlideEffect = Effect_PortaDown;
                                        }
                                    }
                                }
                            }
                            else if ((note.IsMusical || note.IsStop) && prevSlideEffect != Effect_None)
                            {
                                if (prevSlideEffect == Effect_PortaUp ||
                                    prevSlideEffect == Effect_PortaDown ||
                                    prevSlideEffect == Effect_Portamento)
                                {
                                    effectString += $" {EffectToTextLookup[prevSlideEffect]}00";
                                }

                                prevSlideEffect = Effect_None;
                            }

                            if (time == patternLen - 1)
                            {
                                if (p == song.Length - 1 && song.LoopPoint >= 0)
                                {
                                    effectString += $" B{song.LoopPoint:X2}";
                                }
                                else if (patternLen != song.PatternLength)
                                {
                                    effectString += $" D00";
                                }
                            }

                            if (note.HasSpeed)
                            {
                                effectString += $" F{note.Speed:X2}";
                            }
                            if (note.HasVibrato)
                            {
                                effectString += $" 4{VibratoSpeedExportLookup[note.VibratoSpeed]:X1}{note.VibratoDepth:X1}";
                            }
                            if (note.HasFinePitch)
                            {
                                effectString += $" P{(byte)(-note.FinePitch + 0x80):X2}";
                            }
                            if (note.HasFdsModDepth)
                            {
                                effectString += $" H{note.FdsModDepth:X2}";
                            }
                            if (note.HasFdsModSpeed)
                            {
                                effectString += $" I{(note.FdsModSpeed >> 8) & 0xff:X2}";
                                effectString += $" J{(note.FdsModSpeed >> 0) & 0xff:X2}";
                            }

                            while (effectString.Length < 12)
                            {
                                effectString += " ...";
                            }

                            if (noAttack)
                            {
                                noteString       = "...";
                                instrumentString = "..";
                            }

                            line = $" : {noteString} {instrumentString} {volumeString}{effectString}";

                            if (note.IsMusical || note.IsStop)
                            {
                                prevNoteValue = note.IsSlideNote ? note.SlideNoteTarget : note.Value;
                                if (note.IsMusical)
                                {
                                    prevInstrument = note.Instrument;
                                }
                            }

                            patternLines.Add(line);
                        }

                        patternRows[pattern] = patternLines;
                    }
                }

                for (int j = 0; j < maxPatternCount; j++)
                {
                    lines.Add($"PATTERN {j:X2}");

                    for (int p = 0; p < song.PatternLength; p++)
                    {
                        var line = $"ROW {p:X2}";
                        for (int c = 0; c < song.Channels.Length; c++)
                        {
                            var channel = song.Channels[c];

                            if (j >= channel.Patterns.Count)
                            {
                                line += " : ... .. . ... ... ...";
                            }
                            else
                            {
                                line += patternRows[channel.Patterns[j]][p];
                            }
                        }

                        lines.Add(line);
                    }

                    lines.Add("");
                }
            }

            lines.Add("# End of export");

            File.WriteAllLines(filename, lines);

            return(true);
        }