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