private void SetupProject(Project originalProject, int[] songIds) { // Work on a temporary copy. project = originalProject.Clone(); project.Filename = originalProject.Filename; for (int i = 0; i < project.Songs.Count; i++) { if (!songIds.Contains(project.Songs[i].Id)) { project.DeleteSong(project.Songs[i]); i--; } } project.DeleteUnusedInstruments(); }
private void SetupProject(Project originalProject, int[] songIds) { // Work on a temporary copy. project = originalProject.Clone(); project.Filename = originalProject.Filename; // NULL = All songs. if (songIds != null) { for (int i = 0; i < project.Songs.Count; i++) { if (!songIds.Contains(project.Songs[i].Id)) { project.DeleteSong(project.Songs[i]); i--; } } } // Remove features not supported by famitone, keeps the rest of the processing simpler. if (kernel == FamiToneKernel.FamiTone2) { foreach (var song in project.Songs) { foreach (var channel in song.Channels) { foreach (var pattern in channel.Patterns) { for (int i = 0; i < song.PatternLength; i++) { pattern.Notes[i].HasVolume = false; } } } } } project.DeleteUnusedInstruments(); }
private void SetupProject(Project originalProject, int[] songIds) { // Work on a temporary copy. project = originalProject.Clone(); project.Filename = originalProject.Filename; // NULL = All songs. if (songIds != null) { for (int i = 0; i < project.Songs.Count; i++) { if (!songIds.Contains(project.Songs[i].Id)) { project.DeleteSong(project.Songs[i]); i--; } } } RemoveUnsupportedFeatures(); project.DeleteUnusedInstruments(); }
public unsafe static bool Save(Project originalProject, string filename, int[] songIds, string name, string author, string copyright) { try { if (songIds.Length == 0) { return(false); } var project = originalProject.Clone(); project.RemoveAllSongsBut(songIds); using (var file = new FileStream(filename, FileMode.Create)) { // Header var header = new NsfHeader(); header.id[0] = (byte)'N'; header.id[1] = (byte)'E'; header.id[2] = (byte)'S'; header.id[3] = (byte)'M'; header.id[4] = (byte)0x1a; header.version = 1; header.numSongs = (byte)project.Songs.Count; header.startingSong = 1; header.loadAddr = 0x8000; header.initAddr = NsfInitAddr; header.playAddr = NsfPlayAddr; header.playSpeedNTSC = 16639; header.playSpeedPAL = 19997; header.banks[0] = 0; header.banks[1] = 1; header.banks[2] = 2; header.banks[3] = 3; header.banks[4] = 4; header.banks[5] = 5; header.banks[6] = 6; header.banks[7] = 7; var nameBytes = Encoding.ASCII.GetBytes(name); var artistBytes = Encoding.ASCII.GetBytes(author); var copyrightBytes = Encoding.ASCII.GetBytes(copyright); Marshal.Copy(nameBytes, 0, new IntPtr(header.song), Math.Min(31, nameBytes.Length)); Marshal.Copy(artistBytes, 0, new IntPtr(header.artist), Math.Min(31, artistBytes.Length)); Marshal.Copy(copyrightBytes, 0, new IntPtr(header.copyright), Math.Min(31, copyrightBytes.Length)); var headerBytes = new byte[sizeof(NsfHeader)]; Marshal.Copy(new IntPtr(&header), headerBytes, 0, headerBytes.Length); file.Write(headerBytes, 0, headerBytes.Length); // Code/sound engine var nsfBinStream = typeof(NsfFile).Assembly.GetManifestResourceStream("FamiStudio.Nsf.nsf.bin"); var nsfBinBuffer = new byte[NsfCodeSize]; nsfBinStream.Read(nsfBinBuffer, 0, nsfBinBuffer.Length); Debug.Assert(nsfBinStream.Length == NsfCodeSize); file.Write(nsfBinBuffer, 0, nsfBinBuffer.Length); var numDpcmPages = 0; var dpcmBaseAddr = NsfDpcmOffset; byte[] dpcmBytes = null; if (project.UsesSamples) { var famiToneFile = new FamitoneMusicFile(); famiToneFile.GetBytes(project, null, 0, NsfDpcmOffset, out _, out dpcmBytes); numDpcmPages = dpcmBytes != null ? (dpcmBytes.Length + NsfPageSize - 1) / NsfPageSize : 0; // 0KB - 4KB samples: starts at 0xf000 // 4KB - 8KB samples: starts at 0xe000 // 8KB - 12KB samples: starts at 0xd000 // 12KB - 16KB samples: starts at 0xc000 dpcmBaseAddr += (4 - numDpcmPages) * 0x1000; } var songTable = new byte[NsfMaxSongs * 4]; var songBytes = new List <byte>(); // Export each song individually, build TOC at the same time. for (int i = 0; i < project.Songs.Count && i < NsfMaxSongs; i++) { var song = project.Songs[i]; int page = songBytes.Count / NsfPageSize + 1; int addr = NsfSongAddr + (songBytes.Count & (NsfPageSize - 1)); var famiToneFile = new FamitoneMusicFile(); famiToneFile.GetBytes(project, new int[] { song.Id }, addr, dpcmBaseAddr, out var currentSongBytes, out _); songTable[i * 4 + 0] = (byte)(page + numDpcmPages); songTable[i * 4 + 1] = (byte)((addr >> 0) & 0xff); songTable[i * 4 + 2] = (byte)((addr >> 8) & 0xff); songTable[i * 4 + 3] = (byte)(numDpcmPages); // TODO: Same value for all songs... No need to be in song table. songBytes.AddRange(currentSongBytes); } // Song table file.Write(songTable, 0, songTable.Length); // DPCM will be on the first 4 pages (1,2,3,4) if (project.UsesSamples && dpcmBytes != null) { if (songBytes.Count > NsfMaxSongSizeDpcm) { // TODO: Error message. return(false); } Array.Resize(ref dpcmBytes, numDpcmPages * NsfPageSize); file.Write(dpcmBytes, 0, dpcmBytes.Length); } else { if (songBytes.Count > NsfMaxSongSize) { // TODO: Error message. return(false); } } // Song file.Write(songBytes.ToArray(), 0, songBytes.Count); file.Flush(); file.Close(); } } catch { return(false); } return(true); }
public unsafe static bool Save(Project originalProject, FamitoneMusicFile.FamiToneKernel kernel, string filename, int[] songIds, string name, string author, string copyright) { try { if (songIds.Length == 0) { return(false); } var project = originalProject.Clone(); project.RemoveAllSongsBut(songIds); // Header var header = new NsfHeader(); header.id[0] = (byte)'N'; header.id[1] = (byte)'E'; header.id[2] = (byte)'S'; header.id[3] = (byte)'M'; header.id[4] = (byte)0x1a; header.version = 1; header.numSongs = (byte)project.Songs.Count; header.startingSong = 1; header.loadAddr = 0x8000; header.initAddr = NsfInitAddr; header.playAddr = NsfPlayAddr; header.playSpeedNTSC = 16639; header.playSpeedPAL = 19997; header.extensionFlags = (byte)(project.ExpansionAudio == Project.ExpansionVrc6 ? 1 : 0); header.banks[0] = 0; header.banks[1] = 1; header.banks[2] = 2; header.banks[3] = 3; header.banks[4] = 4; header.banks[5] = 5; header.banks[6] = 6; header.banks[7] = 7; var nameBytes = Encoding.ASCII.GetBytes(name); var artistBytes = Encoding.ASCII.GetBytes(author); var copyrightBytes = Encoding.ASCII.GetBytes(copyright); Marshal.Copy(nameBytes, 0, new IntPtr(header.song), Math.Min(31, nameBytes.Length)); Marshal.Copy(artistBytes, 0, new IntPtr(header.artist), Math.Min(31, artistBytes.Length)); Marshal.Copy(copyrightBytes, 0, new IntPtr(header.copyright), Math.Min(31, copyrightBytes.Length)); var headerBytes = new byte[sizeof(NsfHeader)]; Marshal.Copy(new IntPtr(&header), headerBytes, 0, headerBytes.Length); List <byte> nsfBytes = new List <byte>(); string kernelBinary; if (kernel == FamitoneMusicFile.FamiToneKernel.FamiTone2FS) { kernelBinary = project.ExpansionAudio == Project.ExpansionVrc6 ? "nsf_ft2_fs_vrc6.bin" : "nsf_ft2_fs.bin"; } else { kernelBinary = "nsf_ft2.bin"; } // Code/sound engine var nsfBinStream = typeof(NsfFile).Assembly.GetManifestResourceStream("FamiStudio.Nsf." + kernelBinary); var nsfBinBuffer = new byte[nsfBinStream.Length]; nsfBinStream.Read(nsfBinBuffer, 0, nsfBinBuffer.Length); nsfBytes.AddRange(nsfBinBuffer); var songTableIdx = nsfBytes.Count; var songTableSize = NsfGlobalVarsSize + project.Songs.Count * NsfSongTableEntrySize; nsfBytes.AddRange(new byte[songTableSize]); var songDataIdx = nsfBytes.Count; var dpcmBaseAddr = NsfDpcmOffset; var dpcmPadding = 0; if (project.UsesSamples) { var totalSampleSize = project.GetTotalSampleSize(); // Samples need to be 64-bytes aligned. nsfBytes.AddRange(new byte[64 - (nsfBytes.Count & 0x3f)]); // We start putting the samples right after the code, so the first page is not a // full one. If we have near 16KB of samples, we might go over the 4 page limit. // In this case, we will introduce padding until the next page. if (nsfBytes.Count + totalSampleSize > Project.MaxSampleSize) { dpcmPadding = NsfPageSize - (nsfBytes.Count & (NsfPageSize - 1)); nsfBytes.AddRange(new byte[dpcmPadding]); } var dpcmPageStart = (nsfBytes.Count) / NsfPageSize; var dpcmPageEnd = (nsfBytes.Count + totalSampleSize) / NsfPageSize; var dpcmPageCount = dpcmPageEnd - dpcmPageStart + 1; // Otherwise we will allocate at least a full page for the samples and use the following mapping: // 0KB - 4KB samples: starts at 0xf000 // 4KB - 8KB samples: starts at 0xe000 // 8KB - 12KB samples: starts at 0xd000 // 12KB - 16KB samples: starts at 0xc000 dpcmBaseAddr += (4 - dpcmPageCount) * NsfPageSize + (nsfBytes.Count & (NsfPageSize - 1)); nsfBytes.AddRange(project.GetPackedSampleData()); nsfBytes[songTableIdx + 0] = (byte)dpcmPageStart; // DPCM_PAGE_START nsfBytes[songTableIdx + 1] = (byte)dpcmPageCount; // DPCM_PAGE_CNT } // Export each song individually, build TOC at the same time. for (int i = 0; i < project.Songs.Count; i++) { var song = project.Songs[i]; var firstPage = nsfBytes.Count < NsfPageSize; int page = nsfBytes.Count / NsfPageSize + (firstPage ? 1 : 0); int addr = NsfMemoryStart + (firstPage ? 0 : NsfPageSize) + (nsfBytes.Count & (NsfPageSize - 1)); var songBytes = new FamitoneMusicFile(kernel).GetBytes(project, new int[] { song.Id }, addr, dpcmBaseAddr); // If we introduced padding for the samples, we can try to squeeze a song in there. if (songBytes.Length < dpcmPadding) { // TODO. We should start writing at [songDataIdx] until we run out of dpcmPadding. } var idx = songTableIdx + NsfGlobalVarsSize + i * NsfSongTableEntrySize; nsfBytes[idx + 0] = (byte)(page); nsfBytes[idx + 1] = (byte)((addr >> 0) & 0xff); nsfBytes[idx + 2] = (byte)((addr >> 8) & 0xff); nsfBytes[idx + 3] = (byte)0; nsfBytes.AddRange(songBytes); } // Finally insert the header, not very efficient, but easy. nsfBytes.InsertRange(0, headerBytes); File.WriteAllBytes(filename, nsfBytes.ToArray()); } catch { return(false); } return(true); }
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 static bool Save(Project originalProject, string filename) { var project = originalProject.Clone(); ConvertPitchEnvelopes(project); var envelopes = MergeIdenticalEnvelopes(project); var lines = new List <string>(); lines.Add("# FamiTracker text export 0.4.2"); lines.Add(""); lines.Add("# Global settings"); lines.Add("MACHINE 0"); lines.Add("FRAMERATE 0"); lines.Add("EXPANSION 0"); lines.Add("VIBRATO 1"); lines.Add("SPLIT 21"); lines.Add(""); lines.Add("# Macros"); for (int i = 0; i < Envelope.Max; i++) { var envArray = envelopes[i]; for (int j = 0; j < envArray.Length; j++) { var env = envArray[j]; lines.Add($"MACRO{i,8} {j,4} {env.Loop,4} -1 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"); 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]; int volEnvIdx = instrument.Envelopes[Envelope.Volume].Length > 0 ? Array.IndexOf(envelopes[Envelope.Volume], instrument.Envelopes[Envelope.Volume]) : -1; int arpEnvIdx = instrument.Envelopes[Envelope.Arpeggio].Length > 0 ? Array.IndexOf(envelopes[Envelope.Arpeggio], instrument.Envelopes[Envelope.Arpeggio]) : -1; int pitEnvIdx = instrument.Envelopes[Envelope.Pitch].Length > 0 ? Array.IndexOf(envelopes[Envelope.Pitch], instrument.Envelopes[Envelope.Pitch]) : -1; lines.Add($"INST2A03{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) { var octave = (i - 1) / 12 + 1; var semitone = (i - 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]; CreateMissingPatterns(song); lines.Add($"TRACK{song.PatternLength,4}{song.Speed,4}{song.Tempo,4} \"{song.Name}\""); lines.Add($"COLUMNS : 1 1 1 1 1"); lines.Add(""); for (int j = 0; j < song.Length; j++) { var line = $"ORDER {j:X2} :"; for (int k = 0; k < Channel.Count; 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); } for (int j = 0; j < maxPatternCount; j++) { lines.Add($"PATTERN {j:X2}"); for (int k = 0; k < song.PatternLength; k++) { var line = $"ROW {k:X2}"; for (int l = 0; l < Channel.Count; l++) { var channel = song.Channels[l]; if (j >= channel.Patterns.Count) { line += " : ... .. . ..."; } else { var pattern = channel.Patterns[j]; var note = pattern.Notes[k]; var noteString = note.IsStop ? "---" : note.IsValid ? GetFamiTrackerNoteName(l, note) : "..."; var instrumentString = note.IsValid && !note.IsStop ? (note.Instrument == null ? project.Instruments.Count : project.Instruments.IndexOf(note.Instrument)).ToString("X2") : ".."; var effectString = "..."; switch (note.Effect) { case Note.EffectJump: effectString = $"B{note.EffectParam:X2}"; break; case Note.EffectSkip: effectString = $"D{note.EffectParam:X2}"; break; case Note.EffectSpeed: effectString = $"F{note.EffectParam:X2}"; break; } line += $" : {noteString} {instrumentString} . {effectString}"; } } lines.Add(line); } lines.Add(""); } } File.WriteAllLines(filename, lines); return(true); }