Beispiel #1
0
        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);
        }
Beispiel #2
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);
        }