示例#1
0
        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();
        }
示例#2
0
        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();
        }
示例#3
0
        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();
        }
示例#4
0
        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);
        }
示例#5
0
文件: NsfFile.cs 项目: VN0/FamiStudio
        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);
        }
示例#6
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);
        }
示例#7
0
        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);
        }