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); }
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); }