public unsafe bool Save(Project originalProject, FamitoneMusicFile.FamiToneKernel kernel, string filename, int[] songIds, string name, string author, string copyright, MachineType mode) { try { if (songIds.Length == 0) { return(false); } Debug.Assert(!originalProject.UsesExpansionAudio || mode == 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)mode; header.extensionFlags = (byte)(project.ExpansionAudio == Project.ExpansionNone ? 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 == FamitoneMusicFile.FamiToneKernel.FamiStudio) { kernelBinary += "_famistudio"; if (project.UsesExpansionAudio) { kernelBinary += $"_{project.ExpansionAudioShortName.ToLower()}"; if (project.ExpansionAudio == Project.ExpansionN163) { kernelBinary += $"_{project.ExpansionNumChannels}ch"; } } } else { kernelBinary += "_famitone2"; } switch (mode) { case MachineType.NTSC: kernelBinary += "_ntsc"; break; case MachineType.PAL: kernelBinary += "_pal"; break; case MachineType.Dual: kernelBinary += "_dual"; break; } if (project.UsesFamiStudioTempo) { kernelBinary += "_tempo"; } 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); 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 - 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 } // 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, mode); // 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, 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); }