Beispiel #1
0
        static public void WaveToDpcm(short[] wave, int minSample, int maxSample, float waveSampleRate, float dpcmSampleRate, int dpcmCounterStart, WaveToDpcmRoundingMode roundMode, out byte[] dpcm)
        {
            if (wave.Length == 0)
            {
                dpcm = new byte[0];
                return;
            }

            var waveNumSamples      = maxSample - minSample;
            var dpcmNumSamplesFloat = waveNumSamples * (dpcmSampleRate / (float)waveSampleRate);
            var dpcmNumSamples      = 0;

            switch (roundMode)
            {
            case WaveToDpcmRoundingMode.RoundTo16Bytes:
                dpcmNumSamples = (int)Math.Round(dpcmNumSamplesFloat / 128.0f) * 128;
                break;

            case WaveToDpcmRoundingMode.RoundTo16BytesPlusOne:
                dpcmNumSamples = (int)Math.Round(dpcmNumSamplesFloat / 128.0f) * 128 + 8;
                break;

            default:
                dpcmNumSamples = (int)Math.Round(dpcmNumSamplesFloat);
                break;
            }

            // Resample to the correct rate
            var resampledWave = new short[dpcmNumSamples];

            Resample(wave, minSample, maxSample, resampledWave);

            var dpcmSize = (dpcmNumSamples + 7) / 8; // Round up to byte.

            dpcm = new byte[dpcmSize];

            // DPCM conversion.
            var dpcmCounter = dpcmCounterStart;
            var lastBit     = 0;

            for (int i = 0; i < resampledWave.Length; i++)
            {
                var up = false;

                if (i != resampledWave.Length - 1)
                {
                    // Is it better to go up or down?
                    var distUp   = Math.Abs(DpcmCounterToWaveSample(Math.Min(dpcmCounter + 1, 63)) - resampledWave[i + 1]);
                    var distDown = Math.Abs(DpcmCounterToWaveSample(Math.Max(dpcmCounter - 1, 0)) - resampledWave[i + 1]);

                    up = distUp < distDown;
                }
                else
                {
                    up = DpcmCounterToWaveSample(dpcmCounter) < resampledWave[i];
                }

                if (up)
                {
                    var index = i / 8;
                    var mask  = (1 << (i & 7));

                    dpcm[index] |= (byte)mask;
                    dpcmCounter  = Math.Min(dpcmCounter + 1, 63);
                    lastBit      = 1;
                }
                else
                {
                    dpcmCounter = Math.Max(dpcmCounter - 1, 0);
                    lastBit     = 0;
                }
            }

            // We might not (fully) write the last byte, so fill with 0x55 or 0xaa.
            for (int i = resampledWave.Length; i < Utils.RoundUp(resampledWave.Length, 8); i++)
            {
                if (lastBit == 0)
                {
                    var index = i / 8;
                    var mask  = (1 << (i & 7));

                    dpcm[index] |= (byte)mask;
                }

                lastBit ^= 1;
            }
        }
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.UsesAnyExpansionAudio || machine == MachineType.NTSC);

            var project = originalProject.DeepClone();
            project.DeleteAllSongsBut(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.UsesAnyExpansionAudio ? project.ExpansionAudioMask : 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);

            var nsfBytes = new List <byte>();

            string kernelBinary = "nsf";
            if (kernel == FamiToneKernel.FamiStudio)
            {
                kernelBinary += "_famistudio";

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

                if (project.UsesSingleExpansionAudio)
                {
                    kernelBinary += $"_{ExpansionType.ShortNames[project.SingleExpansion].ToLower()}";
                }
                else if (project.UsesMultipleExpansionAudios)
                {
                    kernelBinary += $"_multi";

                    if (project.UsesN163Expansion)
                    {
                        kernelBinary += $"_n163";
                    }
                }

                if (project.UsesN163Expansion)
                {
                    kernelBinary += $"_{project.ExpansionNumN163Channels}ch";
                }
            }
            else
            {
                kernelBinary += "_famitone2";
            }

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

            var driverSizeRounded = Utils.RoundUp(nsfBinBuffer.Length, NsfPageSize);

            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;
                nsfBytes[songTableIdx + 1] = (byte)dpcmPageCount;

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

            // This is only used in multi-expansion.
            nsfBytes[songTableIdx + 2] = (byte)project.ExpansionAudioMask;

            // Export each song individually, build TOC at the same time.
            for (int i = 0; i < project.Songs.Count; i++)
            {
                var song = project.Songs[i];

                // If we are in the same page as the driver, the song will start in a 0x8000 address (0x9000 for multi)
                // so we need to increment the page by one so that the NSF driver correctly maps the subsequent pages.
                var samePageAsDriver = nsfBytes.Count < NsfPageSize;
                int page             = nsfBytes.Count / NsfPageSize + (samePageAsDriver ? 1 : 0);
                int addr             = NsfMemoryStart + (samePageAsDriver ? 0 : driverSizeRounded) + (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);
        }