Beispiel #1
0
        public unsafe bool Save(Project originalProject, string filename, int[] songIds, string name, string author)
        {
            try
            {
                if (songIds.Length == 0)
                {
                    return(false);
                }

                if (songIds.Length > MaxSongs)
                {
                    Array.Resize(ref songIds, MaxSongs);
                }

                var project = originalProject.DeepClone();
                project.RemoveAllSongsBut(songIds);

                if (project.ExpansionAudio != ExpansionType.Fds)
                {
                    project.SetExpansionAudio(ExpansionType.Fds);
                }

                string fdsDiskName = "FamiStudio.Rom.fds";
                if (project.UsesFamiTrackerTempo)
                {
                    fdsDiskName += "_famitracker";
                }
                fdsDiskName += ".fds";

                // Read FDS disk header + code.
                var fdsDiskBinStream = typeof(RomFile).Assembly.GetManifestResourceStream(fdsDiskName);
                var fdsDiskInitBytes = new byte[fdsDiskBinStream.Length];
                fdsDiskBinStream.Read(fdsDiskInitBytes, 0, fdsDiskInitBytes.Length);

                TruncateToLastFile(ref fdsDiskInitBytes);

                var fdsFileBytes = new List <byte>();
                fdsFileBytes.AddRange(fdsDiskInitBytes);

                Log.LogMessage(LogSeverity.Info, $"FDS code and graphics files: {fdsDiskInitBytes.Length} bytes.");

                var fileIndex     = FdsFirstFileIndex;
                var dpcmFileIndex = 0;

                // Create the DPCM file if needed.
                if (project.UsesSamples)
                {
                    var dpcmBytes = project.GetPackedSampleData();

                    if (dpcmBytes.Length > FdsMaxDpcmSize)
                    {
                        Log.LogMessage(LogSeverity.Warning, $"DPCM samples size ({dpcmBytes.Length}) is larger than the maximum allowed for FDS export ({FdsMaxDpcmSize}). Truncating.");
                        Array.Resize(ref dpcmBytes, FdsMaxDpcmSize);
                    }

                    AddFile(fdsFileBytes, fileIndex, FdsDpcmStart, "DPCM....", dpcmBytes);

                    dpcmFileIndex = fileIndex;
                    fileIndex++;

                    Log.LogMessage(LogSeverity.Info, $"DPCM file size: {dpcmBytes.Length} bytes.");
                }

                var projectInfo = BuildProjectInfo(songIds, name, author);
                var songTable   = BuildSongTableOfContent(project);

                // Export each song as an individual file.
                for (int i = 0; i < project.Songs.Count; i++)
                {
                    var song      = project.Songs[i];
                    var songBytes = new FamitoneMusicFile(FamiToneKernel.FamiStudio, false).GetBytes(project, new int[] { song.Id }, FdsSongDataAddr, FdsDpcmStart, MachineType.NTSC);

                    songTable[i].page  = (byte)fileIndex;
                    songTable[i].flags = (byte)(song.UsesDpcm ? dpcmFileIndex : 0);

                    if (songBytes.Length > FdsMaxSongSize)
                    {
                        Log.LogMessage(LogSeverity.Warning, $"Song '{song.Name}' is too large ({songBytes.Length} bytes, maximum is {FdsMaxSongSize}). File will be corrupted.");
                        Array.Resize(ref songBytes, FdsMaxSongSize);
                    }

                    if (fdsFileBytes.Count + FdsBlockHeaderSize + songBytes.Length > FdsMaxFileSize)
                    {
                        Log.LogMessage(LogSeverity.Warning, $"Reached maximum file size ({FdsMaxFileSize}). Songs will be missing.");
                        break;
                    }

                    AddFile(fdsFileBytes, fileIndex, FdsSongDataAddr, $"SONG{i}...", songBytes);

                    fileIndex++;

                    Log.LogMessage(LogSeverity.Info, $"Song '{song.Name}' file size: {songBytes.Length} bytes.");
                }

                //File.WriteAllBytes("D:\\dump\\fdsdata.bin", fdsFileBytes.ToArray());

                // Use this field for the number of files.
                projectInfo.dpcmPageCount = (byte)fileIndex;

                // Pad rest with zeroes.
                fdsFileBytes.AddRange(new byte[FdsMaxFileSize - fdsFileBytes.Count]);

                // Build project info + song table of content.
                var tocBytes = new byte[sizeof(RomProjectInfo) + sizeof(RomSongEntry) * songTable.Length];

                Marshal.Copy(new IntPtr(&projectInfo), tocBytes, 0, sizeof(RomProjectInfo));

                for (int i = 0; i < MaxSongs; i++)
                {
                    fixed(RomSongEntry *songEntry = &songTable[i])
                    Marshal.Copy(new IntPtr(songEntry), tocBytes, sizeof(RomProjectInfo) + i * sizeof(RomSongEntry), sizeof(RomSongEntry));
                }

                //File.WriteAllBytes("D:\\dump\\fdstoc.bin", tocBytes);

                // Path TOC file.
                var byteArray = fdsFileBytes.ToArray();
                PatchFile(byteArray, "TOC.....", tocBytes);

                // Build final ROM and save.
                File.WriteAllBytes(filename, byteArray);

                Log.LogMessage(LogSeverity.Info, $"FDS export successful, final file size {byteArray.Length} bytes.");
            }
            catch (Exception e)
            {
                Log.LogMessage(LogSeverity.Error, "Please contact the developer on GitHub!");
                Log.LogMessage(LogSeverity.Error, e.Message);
                Log.LogMessage(LogSeverity.Error, e.StackTrace);
                return(false);
            }

            return(true);
        }
Beispiel #2
0
        public unsafe bool Save(Project originalProject, int kernel, string filename, int[] songIds, string name, string author, string copyright, int machine)
        {
#if !DEBUG
            try
            {
#endif
            if (songIds.Length == 0)
            {
                return(false);
            }

            Debug.Assert(!originalProject.UsesExpansionAudio || machine == MachineType.NTSC);

            var project = originalProject.DeepClone();
            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.palNtscFlags   = (byte)machine;
            header.extensionFlags = (byte)(project.ExpansionAudio == ExpansionType.None ? 0 : 1 << (project.ExpansionAudio - 1));
            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 = "nsf";
            if (kernel == FamiToneKernel.FamiStudio)
            {
                kernelBinary += "_famistudio";

                if (project.UsesFamiTrackerTempo)
                {
                    kernelBinary += "_famitracker";
                }

                if (project.UsesExpansionAudio)
                {
                    kernelBinary += $"_{project.ExpansionAudioShortName.ToLower()}";

                    if (project.ExpansionAudio == ExpansionType.N163)
                    {
                        kernelBinary += $"_{project.ExpansionNumChannels}ch";
                    }
                }
            }
            else
            {
                kernelBinary += "_famitone2";

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

            switch (machine)
            {
            case MachineType.NTSC: kernelBinary += "_ntsc"; break;

            case MachineType.PAL:  kernelBinary += "_pal";  break;

            case MachineType.Dual: kernelBinary += "_dual"; break;
            }

            kernelBinary += ".bin";

            // Code/sound engine
            var nsfBinStream = typeof(NsfFile).Assembly.GetManifestResourceStream("FamiStudio.Nsf." + kernelBinary);
            var nsfBinBuffer = new byte[nsfBinStream.Length - 128];     // Skip header.
            nsfBinStream.Seek(128, SeekOrigin.Begin);
            nsfBinStream.Read(nsfBinBuffer, 0, nsfBinBuffer.Length);

            nsfBytes.AddRange(nsfBinBuffer);

            Log.LogMessage(LogSeverity.Info, $"Sound engine code size: {nsfBinBuffer.Length} bytes.");

            var songTableIdx  = nsfBytes.Count;
            var songTableSize = NsfGlobalVarsSize + project.Songs.Count * NsfSongTableEntrySize;

            nsfBytes.AddRange(new byte[songTableSize]);

            Log.LogMessage(LogSeverity.Info, $"Song table size: {songTableSize} bytes.");

            var songDataIdx  = nsfBytes.Count;
            var dpcmBaseAddr = NsfDpcmOffset;
            var dpcmPadding  = 0;

            if (project.UsesSamples)
            {
                var totalSampleSize = project.GetTotalSampleSize();

                // Samples need to be 64-bytes aligned.
                var initPaddingSize = 64 - (nsfBytes.Count & 0x3f);
                nsfBytes.AddRange(new byte[initPaddingSize]);

                // 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.MaxMappedSampleSize)
                {
                    dpcmPadding = NsfPageSize - (nsfBytes.Count & (NsfPageSize - 1));
                    nsfBytes.AddRange(new byte[dpcmPadding]);
                }

                var dpcmPageStart = (nsfBytes.Count) / NsfPageSize;
                var dpcmPageEnd   = (nsfBytes.Count + totalSampleSize - 1) / 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

                Log.LogMessage(LogSeverity.Info, $"DPCM samples size: {totalSampleSize} bytes.");
                Log.LogMessage(LogSeverity.Info, $"DPCM padding size: {initPaddingSize + dpcmPadding} bytes.");
            }

            // 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, false).GetBytes(project, new int[] { song.Id }, addr, dpcmBaseAddr, machine);

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

                Log.LogMessage(LogSeverity.Info, $"Song '{song.Name}' size: {songBytes.Length} bytes.");
            }

            // Finally insert the header, not very efficient, but easy.
            nsfBytes.InsertRange(0, headerBytes);

            File.WriteAllBytes(filename, nsfBytes.ToArray());

            Log.LogMessage(LogSeverity.Info, $"NSF export successful, final file size {nsfBytes.Count} bytes.");
#if !DEBUG
        }

        catch (Exception e)
        {
            Log.LogMessage(LogSeverity.Error, "Please contact the developer on GitHub!");
            Log.LogMessage(LogSeverity.Error, e.Message);
            Log.LogMessage(LogSeverity.Error, e.StackTrace);
            return(false);
        }
#endif

            return(true);
        }
Beispiel #3
0
        const int MaxDpcmSize         = 0x2c00; // 11KB

        public unsafe bool Save(Project originalProject, string filename, int[] songIds, string name, string author, bool pal)
        {
#if !DEBUG
            try
#endif
            {
                if (songIds.Length == 0)
                {
                    return(false);
                }

                Debug.Assert(!originalProject.UsesAnyExpansionAudio || !pal);

                if (songIds.Length > MaxSongs)
                {
                    Array.Resize(ref songIds, MaxSongs);
                }

                var project = originalProject.DeepClone();
                project.DeleteAllSongsBut(songIds);
                project.SetExpansionAudioMask(ExpansionType.NoneMask);

                var headerBytes = new byte[RomHeaderLength];
                var codeBytes   = new byte[RomCodeAndTocSize + RomTileSize];

                // Load ROM header (16 bytes) + code/tiles (12KB).
                string romName = "FamiStudio.Rom.rom";
                if (project.UsesFamiTrackerTempo)
                {
                    romName += "_famitracker";
                }
                romName += pal ? "_pal" : "_ntsc";
                romName += ".nes";

                var romBinStream = typeof(RomFile).Assembly.GetManifestResourceStream(romName);
                romBinStream.Read(headerBytes, 0, RomHeaderLength);
                romBinStream.Seek(-RomCodeAndTocSize - RomTileSize, SeekOrigin.End);
                romBinStream.Read(codeBytes, 0, RomCodeAndTocSize + RomTileSize);

                Log.LogMessage(LogSeverity.Info, $"ROM code and graphics size: {codeBytes.Length} bytes.");

                // Build project info + song table of content.
                var projectInfo = BuildProjectInfo(songIds, name, author);
                var songTable   = BuildSongTableOfContent(project);

                // Gathersong data.
                var songBanks = new List <List <byte> >();

                // Export each song individually, build TOC at the same time.
                for (int i = 0; i < project.Songs.Count; i++)
                {
                    if (i == MaxSongs)
                    {
                        Log.LogMessage(LogSeverity.Warning, $"Too many songs. There is a hard limit of {MaxSongs} at the moment. Ignoring any extra songs.");
                        break;
                    }

                    var song      = project.Songs[i];
                    var songBytes = new FamitoneMusicFile(FamiToneKernel.FamiStudio, false).GetBytes(project, new int[] { song.Id }, RomSongDataStart, RomDpcmStart, pal ? MachineType.PAL : MachineType.NTSC);

                    if (songBytes.Length > MaxSongSize)
                    {
                        Log.LogMessage(LogSeverity.Warning, $"Song {song.Name} has a size of {songBytes.Length}, which is larger than the maximum allowed for ROM export ({MaxSongSize}). Truncating.");
                        Array.Resize(ref songBytes, MaxSongSize);
                    }

                    var numBanks = Utils.DivideAndRoundUp(songBytes.Length, RomBankSize);
                    Debug.Assert(numBanks <= 2);

                    var songBank = songBanks.Count;
                    var songAddr = RomSongDataStart;

                    // If single bank, look for an existing bank with some free space at the end.
                    if (numBanks == 1)
                    {
                        var foundExistingBank = false;

                        for (int j = 0; j < songBanks.Count; j++)
                        {
                            var freeSpace = RomBankSize - songBanks[j].Count;
                            if (songBytes.Length <= freeSpace)
                            {
                                songBank  = j;
                                songAddr  = RomSongDataStart + songBanks[j].Count;
                                songBytes = new FamitoneMusicFile(FamiToneKernel.FamiStudio, false).GetBytes(project, new int[] { song.Id }, songAddr, RomDpcmStart, pal ? MachineType.PAL : MachineType.NTSC);
                                Debug.Assert(songBytes.Length <= freeSpace);
                                foundExistingBank = true;
                                break;
                            }
                        }

                        // No free space found, allocation a new partial bank.
                        if (!foundExistingBank)
                        {
                            songBanks.Add(new List <byte>());
                        }

                        songBanks[songBank].AddRange(songBytes);
                    }
                    else
                    {
                        // When a song uses 2 banks, allocate a new full one and a partial one.
                        var bank0 = new List <byte>();
                        var bank1 = new List <byte>();

                        for (int j = 0; j < RomBankSize; j++)
                        {
                            bank0.Add(songBytes[j]);
                        }
                        for (int j = RomBankSize; j < songBytes.Length; j++)
                        {
                            bank1.Add(songBytes[j]);
                        }

                        songBanks.Add(bank0);
                        songBanks.Add(bank1);
                    }

                    songTable[i].bank    = (byte)songBank;
                    songTable[i].address = (ushort)songAddr;
                    songTable[i].flags   = (byte)(song.UsesDpcm ? 1 : 0);

                    Log.LogMessage(LogSeverity.Info, $"Song '{song.Name}' size: {songBytes.Length} bytes.");
                }

                //File.WriteAllBytes("D:\\debug.bin", songDataBytes.ToArray());

                // Add extra empty banks if we haven't reached the minimum.
                if (songBanks.Count < RomMinNumberBanks)
                {
                    for (int i = songBanks.Count; i < RomMinNumberBanks; i++)
                    {
                        songBanks.Add(new List <byte>());
                    }
                }
                else if ((songBanks.Count & 1) != 0)
                {
                    songBanks.Add(new List <byte>());
                }

                // Build final song bank data.
                var songBanksBytes = new byte[songBanks.Count * RomBankSize];
                for (int i = 0; i < songBanks.Count; i++)
                {
                    Array.Copy(songBanks[i].ToArray(), 0, songBanksBytes, i * RomBankSize, songBanks[i].Count);
                }

                // Patch in code (project info and song table are after the code, 0xf000).
                Marshal.Copy(new IntPtr(&projectInfo), codeBytes, RomTocOffset, sizeof(RomProjectInfo));

                for (int i = 0; i < MaxSongs; i++)
                {
                    fixed(RomSongEntry *songEntry = &songTable[i])
                    Marshal.Copy(new IntPtr(songEntry), codeBytes, RomTocOffset + sizeof(RomProjectInfo) + i * sizeof(RomSongEntry), sizeof(RomSongEntry));
                }

                // Patch header (iNES header always counts in 16KB banks, MMC3 counts in 8KB banks)
                headerBytes[RomHeaderPrgOffset] = (byte)((songBanks.Count + RomCodeDpcmNumBanks) * RomBankSize / 0x4000);

                // Build final ROM and save.
                var romBytes = new List <byte>();
                romBytes.AddRange(headerBytes);
                romBytes.AddRange(songBanksBytes);

                // Samples are at the end, right before the source engine code. MMC3 second to last and last banks respectively.
                if (project.UsesSamples)
                {
                    // Since we keep the code/engine at f000 all the time, we are limited to 12KB of samples in ROM.
                    var dpcmBytes = project.GetPackedSampleData();

                    Log.LogMessage(LogSeverity.Info, $"DPCM size: {dpcmBytes.Length} bytes.");

                    if (dpcmBytes.Length > MaxDpcmSize)
                    {
                        Log.LogMessage(LogSeverity.Warning, $"DPCM samples size ({dpcmBytes.Length}) is larger than the maximum allowed for ROM export ({MaxDpcmSize}). Truncating.");
                    }

                    // Always allocate the full 11KB of samples.
                    Array.Resize(ref dpcmBytes, MaxDpcmSize);

                    romBytes.AddRange(dpcmBytes);
                }
                else
                {
                    romBytes.AddRange(new byte[MaxDpcmSize]);
                }

                romBytes.AddRange(codeBytes);

                File.WriteAllBytes(filename, romBytes.ToArray());

                Log.LogMessage(LogSeverity.Info, $"ROM export successful, final file size {romBytes.Count} bytes.");
            }
#if !DEBUG
            catch (Exception e)
            {
                Log.LogMessage(LogSeverity.Error, "Please contact the developer on GitHub!");
                Log.LogMessage(LogSeverity.Error, e.Message);
                Log.LogMessage(LogSeverity.Error, e.StackTrace);
                return(false);
            }
#endif

            return(true);
        }
Beispiel #4
0
        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);
        }
Beispiel #5
0
        public unsafe static bool Save(Project originalProject, string filename, int[] songIds, string name, string author, bool pal)
        {
            try
            {
                if (songIds.Length == 0)
                {
                    return(false);
                }

                Debug.Assert(!originalProject.UsesExpansionAudio || !pal);

                if (songIds.Length > MaxSongs)
                {
                    Array.Resize(ref songIds, MaxSongs);
                }

                var project = originalProject.DeepClone();
                project.RemoveAllSongsBut(songIds);
                project.SetExpansionAudio(Project.ExpansionNone);

                var headerBytes = new byte[RomHeaderLength];
                var codeBytes   = new byte[RomCodeSize + RomTileSize];

                // Load ROM header (16 bytes) + code/tiles (12KB).
                Stream romBinStream = null;

                string romName = "FamiStudio.Rom.rom";
                romName += pal ? "_pal" : "_ntsc";
                if (project.UsesFamiStudioTempo)
                {
                    romName += "_tempo";
                }
                romName += ".nes";

                romBinStream = typeof(RomFile).Assembly.GetManifestResourceStream(romName);
                romBinStream.Read(headerBytes, 0, RomHeaderLength);
                romBinStream.Seek(-RomCodeSize - RomTileSize, SeekOrigin.End);
                romBinStream.Read(codeBytes, 0, RomCodeSize + RomTileSize);

                // Build project info + song table of content.
                var projectInfo = new RomProjectInfo();
                projectInfo.maxSong = (byte)(songIds.Length - 1);
                Marshal.Copy(EncodeAndCenterString(name), 0, new IntPtr(projectInfo.name), 28);
                Marshal.Copy(EncodeAndCenterString(author), 0, new IntPtr(projectInfo.author), 28);

                var songTable = new RomSongEntry[MaxSongs];
                for (int i = 0; i < project.Songs.Count; i++)
                {
                    fixed(RomSongEntry *songEntry = &songTable[i])
                    {
                        songEntry->page    = 0;
                        songEntry->address = 0x8000;
                        Marshal.Copy(EncodeAndCenterString(project.Songs[i].Name), 0, new IntPtr(songEntry->name), 28);
                    }
                }

                // Gather DPCM + song data.
                var songDataBytes = new List <byte>();
                var dpcmBaseAddr  = RomDpcmOffset;

                // We will put samples right at the beginning.
                if (project.UsesSamples)
                {
                    // Since we keep the code/engine at f000 all the time, we are limited to 12KB of samples in ROM.
                    var totalSampleSize = project.GetTotalSampleSize();
                    var dpcmPageCount   = Math.Min(MaxDpcmPages, (totalSampleSize + (RomPageSize - 1)) / RomPageSize);

                    // Otherwise we will allocate at least a full page for the samples and use the following mapping:
                    //    0KB -  4KB samples: starts at 0xe000
                    //    4KB -  8KB samples: starts at 0xd000
                    //    8KB - 12KB samples: starts at 0xc000
                    dpcmBaseAddr += (MaxDpcmPages - dpcmPageCount) * RomPageSize;

                    var dpcmBytes = project.GetPackedSampleData();
                    if (dpcmBytes.Length > (MaxDpcmPages * RomPageSize))
                    {
                        Array.Resize(ref dpcmBytes, MaxDpcmPages * RomPageSize);
                    }

                    songDataBytes.AddRange(dpcmBytes);

                    projectInfo.dpcmPageCount = (byte)dpcmPageCount;
                    projectInfo.dpcmPageStart = (byte)0;
                }

                // Export each song individually, build TOC at the same time.
                for (int i = 0; i < project.Songs.Count; i++)
                {
                    var song      = project.Songs[i];
                    int page      = songDataBytes.Count / RomPageSize;
                    int addr      = RomMemoryStart + (songDataBytes.Count & (RomPageSize - 1));
                    var songBytes = new FamitoneMusicFile(FamitoneMusicFile.FamiToneKernel.FamiStudio).GetBytes(project, new int[] { song.Id }, addr, dpcmBaseAddr, MachineType.NTSC);

                    songTable[i].page    = (byte)(page);
                    songTable[i].address = (ushort)(addr);
                    songTable[i].flags   = (byte)(song.UsesDpcm ? 1 : 0);

                    songDataBytes.AddRange(songBytes);
                }

                //File.WriteAllBytes("D:\\debug.bin", songDataBytes.ToArray());

                int numPrgBanks = RomMinSize / RomPrgBankSize;

                if (songDataBytes.Count > (RomMinSize - RomCodeSize))
                {
                    numPrgBanks = (songDataBytes.Count + (RomPrgBankSize - 1)) / RomPrgBankSize;
                }

                int padding = (numPrgBanks * RomPrgBankSize) - RomCodeSize - songDataBytes.Count;
                songDataBytes.AddRange(new byte[padding]);

                // Patch in code (project info and song table are at the beginning, 0xf000).
                Marshal.Copy(new IntPtr(&projectInfo), codeBytes, 0, sizeof(RomProjectInfo));

                for (int i = 0; i < MaxSongs; i++)
                {
                    fixed(RomSongEntry *songEntry = &songTable[i])
                    Marshal.Copy(new IntPtr(songEntry), codeBytes, sizeof(RomProjectInfo) + i * sizeof(RomSongEntry), sizeof(RomSongEntry));
                }

                // Patch header.
                headerBytes[RomHeaderPrgOffset] = (byte)numPrgBanks;

                // Build final ROM and save.
                var romBytes = new List <byte>();
                romBytes.AddRange(headerBytes);
                romBytes.AddRange(songDataBytes);
                romBytes.AddRange(codeBytes);

                File.WriteAllBytes(filename, romBytes.ToArray());
            }
            catch
            {
                return(false);
            }

            return(true);
        }
Beispiel #6
0
        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);

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

                    string kernelBinary = kernel == FamitoneMusicFile.FamiToneKernel.FamiTone2 ? "nsf_ft2.bin" : "nsf_ft2_fs.bin";

                    // Code/sound engine
                    var nsfBinStream = typeof(NsfFile).Assembly.GetManifestResourceStream("FamiStudio.Nsf." + kernelBinary);
                    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(kernel);
                        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(kernel);
                        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);
        }
Beispiel #7
0
        public unsafe bool Save(Project originalProject, string filename, int[] songIds, string name, string author, bool pal)
        {
            try
            {
                if (songIds.Length == 0)
                {
                    return(false);
                }

                Debug.Assert(!originalProject.UsesExpansionAudio || !pal);

                if (songIds.Length > MaxSongs)
                {
                    Array.Resize(ref songIds, MaxSongs);
                }

                var project = originalProject.DeepClone();
                project.RemoveAllSongsBut(songIds);
                project.SetExpansionAudio(ExpansionType.None);

                var headerBytes = new byte[RomHeaderLength];
                var codeBytes   = new byte[RomCodeSize + RomTileSize];

                // Load ROM header (16 bytes) + code/tiles (12KB).
                string romName = "FamiStudio.Rom.rom";
                if (project.UsesFamiTrackerTempo)
                {
                    romName += "_famitracker";
                }
                romName += pal ? "_pal" : "_ntsc";
                romName += ".nes";

                var romBinStream = typeof(RomFile).Assembly.GetManifestResourceStream(romName);
                romBinStream.Read(headerBytes, 0, RomHeaderLength);
                romBinStream.Seek(-RomCodeSize - RomTileSize, SeekOrigin.End);
                romBinStream.Read(codeBytes, 0, RomCodeSize + RomTileSize);

                Log.LogMessage(LogSeverity.Info, $"ROM code and graphics size: {codeBytes.Length} bytes.");

                // Build project info + song table of content.
                var projectInfo = BuildProjectInfo(songIds, name, author);
                var songTable   = BuildSongTableOfContent(project);

                // Gather DPCM + song data.
                var songDataBytes = new List <byte>();
                var dpcmBaseAddr  = RomDpcmOffset;

                // We will put samples right at the beginning.
                if (project.UsesSamples)
                {
                    // Since we keep the code/engine at f000 all the time, we are limited to 12KB of samples in ROM.
                    var totalSampleSize = project.GetTotalSampleSize();
                    var dpcmPageCount   = Math.Min(MaxDpcmPages, (totalSampleSize + (RomPageSize - 1)) / RomPageSize);

                    // Otherwise we will allocate at least a full page for the samples and use the following mapping:
                    //    0KB -  4KB samples: starts at 0xe000
                    //    4KB -  8KB samples: starts at 0xd000
                    //    8KB - 12KB samples: starts at 0xc000
                    dpcmBaseAddr += (MaxDpcmPages - dpcmPageCount) * RomPageSize;

                    var dpcmBytes = project.GetPackedSampleData();
                    if (dpcmBytes.Length > (MaxDpcmPages * RomPageSize))
                    {
                        Log.LogMessage(LogSeverity.Warning, $"DPCM samples size ({dpcmBytes.Length}) is larger than the maximum allowed for ROM export ({MaxDpcmPages * RomPageSize}). Truncating.");
                        Array.Resize(ref dpcmBytes, MaxDpcmPages * RomPageSize);
                    }

                    songDataBytes.AddRange(dpcmBytes);

                    projectInfo.dpcmPageCount = (byte)dpcmPageCount;
                    projectInfo.dpcmPageStart = (byte)0;

                    Log.LogMessage(LogSeverity.Info, $"DPCM allocated size: {dpcmPageCount * RomPageSize} bytes.");
                }

                // Export each song individually, build TOC at the same time.
                for (int i = 0; i < project.Songs.Count; i++)
                {
                    var song      = project.Songs[i];
                    int page      = songDataBytes.Count / RomPageSize;
                    int addr      = RomMemoryStart + (songDataBytes.Count & (RomPageSize - 1));
                    var songBytes = new FamitoneMusicFile(FamiToneKernel.FamiStudio, false).GetBytes(project, new int[] { song.Id }, addr, dpcmBaseAddr, pal ? MachineType.PAL : MachineType.NTSC);

                    songTable[i].page    = (byte)(page);
                    songTable[i].address = (ushort)(addr);
                    songTable[i].flags   = (byte)(song.UsesDpcm ? 1 : 0);

                    songDataBytes.AddRange(songBytes);

                    Log.LogMessage(LogSeverity.Info, $"Song '{song.Name}' size: {songBytes.Length} bytes.");
                }

                //File.WriteAllBytes("D:\\debug.bin", songDataBytes.ToArray());

                int numPrgBanks = RomMinSize / RomPrgBankSize;

                if (songDataBytes.Count > (RomMinSize - RomCodeSize))
                {
                    numPrgBanks = (songDataBytes.Count + (RomPrgBankSize - 1)) / RomPrgBankSize;
                }

                int padding = (numPrgBanks * RomPrgBankSize) - RomCodeSize - songDataBytes.Count;
                songDataBytes.AddRange(new byte[padding]);

                // Patch in code (project info and song table are at the beginning, 0xf000).
                Marshal.Copy(new IntPtr(&projectInfo), codeBytes, 0, sizeof(RomProjectInfo));

                for (int i = 0; i < MaxSongs; i++)
                {
                    fixed(RomSongEntry *songEntry = &songTable[i])
                    Marshal.Copy(new IntPtr(songEntry), codeBytes, sizeof(RomProjectInfo) + i * sizeof(RomSongEntry), sizeof(RomSongEntry));
                }

                // Patch header.
                headerBytes[RomHeaderPrgOffset] = (byte)numPrgBanks;

                // Build final ROM and save.
                var romBytes = new List <byte>();
                romBytes.AddRange(headerBytes);
                romBytes.AddRange(songDataBytes);
                romBytes.AddRange(codeBytes);

                File.WriteAllBytes(filename, romBytes.ToArray());

                Log.LogMessage(LogSeverity.Info, $"ROM export successful, final file size {romBytes.Count} bytes.");
            }
            catch (Exception e)
            {
                Log.LogMessage(LogSeverity.Error, "Please contact the developer on GitHub!");
                Log.LogMessage(LogSeverity.Error, e.Message);
                Log.LogMessage(LogSeverity.Error, e.StackTrace);
                return(false);
            }

            return(true);
        }