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