private void UpdateVectorAddresses() { bool isNsf = InteropEmu.GetRomInfo().Format == RomFormat.Nsf; if (isNsf) { NsfHeader header = InteropEmu.NsfGetHeader(); mnuGoToInitHandler.Text = "Init Handler ($" + header.InitAddress.ToString("X4") + ")"; mnuGoToPlayHandler.Text = "Play Handler ($" + header.PlayAddress.ToString("X4") + ")"; } else { int nmiHandler = InteropEmu.DebugGetMemoryValue(DebugMemoryType.CpuMemory, 0xFFFA) | (InteropEmu.DebugGetMemoryValue(DebugMemoryType.CpuMemory, 0xFFFB) << 8); int resetHandler = InteropEmu.DebugGetMemoryValue(DebugMemoryType.CpuMemory, 0xFFFC) | (InteropEmu.DebugGetMemoryValue(DebugMemoryType.CpuMemory, 0xFFFD) << 8); int irqHandler = InteropEmu.DebugGetMemoryValue(DebugMemoryType.CpuMemory, 0xFFFE) | (InteropEmu.DebugGetMemoryValue(DebugMemoryType.CpuMemory, 0xFFFF) << 8); mnuGoToNmiHandler.Text = "NMI Handler ($" + nmiHandler.ToString("X4") + ")"; mnuGoToResetHandler.Text = "Reset Handler ($" + resetHandler.ToString("X4") + ")"; mnuGoToIrqHandler.Text = "IRQ Handler ($" + irqHandler.ToString("X4") + ")"; } mnuGoToInitHandler.Visible = isNsf; mnuGoToPlayHandler.Visible = isNsf; mnuGoToIrqHandler.Visible = !isNsf; mnuGoToNmiHandler.Visible = !isNsf; mnuGoToResetHandler.Visible = !isNsf; }
private void UpdateTrackDisplay() { NsfHeader header = InteropEmu.NsfGetHeader(); int currentTrack = InteropEmu.NsfGetCurrentTrack(); string[] trackNames = header.GetTrackNames(); if (header.TotalSongs != cboTrack.Items.Count) { _trackList = new List <ComboboxItem>(); for (int i = 0; i < header.TotalSongs; i++) { string trackName = (i + 1).ToString(); if (trackNames.Length > 1 && trackNames.Length > i) { trackName += " - " + (string.IsNullOrWhiteSpace(trackNames[i]) ? ResourceHelper.GetMessage("NsfUnnamedTrack") : trackNames[i]); } TimeSpan trackTime = GetTrackLength(header, i); if (trackTime.Ticks > 0) { trackName += " (" + trackTime.ToString(trackTime.TotalHours < 1 ? @"mm\:ss" : @"hh\:mm\:ss") + ")"; } _trackList.Add(new ComboboxItem { Value = i + 1, Description = trackName }); } cboTrack.DataSource = _trackList; cboTrack.DisplayMember = "Value"; } cboTrack.SelectedIndex = currentTrack; lblTrackTotal.Text = "/ " + header.TotalSongs.ToString(); }
private void UpdateTimeDisplay(int frameCount) { if (!InteropEmu.IsNsf()) { _frameCount = 0; return; } NsfHeader header = InteropEmu.NsfGetHeader(); int currentTrack = InteropEmu.NsfGetCurrentTrack(); TimeSpan time = TimeSpan.FromSeconds((double)frameCount / ((header.Flags & 0x01) == 0x01 ? 50.006978 : 60.098812)); string label = time.ToString(time.TotalHours < 1 ? @"mm\:ss" : @"hh\:mm\:ss"); TimeSpan trackTime = GetTrackLength(header, currentTrack); if (trackTime.Ticks > 0) { label += " / " + trackTime.ToString(trackTime.TotalHours < 1 ? @"mm\:ss" : @"hh\:mm\:ss"); } string[] trackNames = header.GetTrackNames(); if (trackNames.Length > 1 && trackNames.Length > currentTrack) { label += Environment.NewLine + (string.IsNullOrWhiteSpace(trackNames[currentTrack]) ? ResourceHelper.GetMessage("NsfUnnamedTrack") : trackNames[currentTrack]); } lblRecording.Visible = lblRecordingDot.Visible = InteropEmu.WaveIsRecording(); lblFastForward.Visible = lblFastForwardIcon.Visible = InteropEmu.GetEmulationSpeed() > 100 || InteropEmu.GetEmulationSpeed() == 0 || InteropEmu.CheckFlag(EmulationFlags.Turbo); lblSlowMotion.Visible = lblSlowMotionIcon.Visible = InteropEmu.GetEmulationSpeed() < 100 && InteropEmu.GetEmulationSpeed() > 0 && !InteropEmu.CheckFlag(EmulationFlags.Turbo); lblTime.Text = label; }
private void UpdateTimeDisplay(int frameCount) { if (!InteropEmu.IsNsf()) { _frameCount = 0; return; } NsfHeader header = InteropEmu.NsfGetHeader(); int currentTrack = InteropEmu.NsfGetCurrentTrack(); TimeSpan time = TimeSpan.FromSeconds((double)frameCount / ((header.Flags & 0x01) == 0x01 ? 50.006978 : 60.098812)); string label = time.ToString(time.TotalHours < 1 ? @"mm\:ss" : @"hh\:mm\:ss"); TimeSpan trackTime = GetTrackLength(header, currentTrack); if (trackTime.Ticks > 0) { label += " / " + trackTime.ToString(trackTime.TotalHours < 1 ? @"mm\:ss" : @"hh\:mm\:ss"); } string[] trackNames = header.GetTrackNames(); if (trackNames.Length > 1 && trackNames.Length > currentTrack) { label += Environment.NewLine + (string.IsNullOrWhiteSpace(trackNames[currentTrack]) ? ResourceHelper.GetMessage("NsfUnnamedTrack") : trackNames[currentTrack]); } lblTime.Text = label; }
private void UpdateTimeDisplay() { if (!_isNsf) { return; } UInt32 elapsedFrames = InteropEmu.NsfGetFrameCount(); NsfHeader header = InteropEmu.NsfGetHeader(); int currentTrack = InteropEmu.NsfGetCurrentTrack(); if (_selectedTrack != currentTrack) { if (!_disableShortcutKeys) { cboTrack.SelectedIndexChanged -= cboTrack_SelectedIndexChanged; cboTrack.SelectedIndex = currentTrack; cboTrack.SelectedIndexChanged += cboTrack_SelectedIndexChanged; } _selectedTrack = currentTrack; } TimeSpan time = TimeSpan.FromSeconds((double)elapsedFrames / ((header.Flags & 0x01) == 0x01 ? 50.006978 : 60.098812)); string label = time.ToString(time.TotalHours < 1 ? @"mm\:ss" : @"hh\:mm\:ss"); TimeSpan trackTime = GetTrackLength(header, currentTrack); if (trackTime.Ticks > 0) { label += " / " + trackTime.ToString(trackTime.TotalHours < 1 ? @"mm\:ss" : @"hh\:mm\:ss"); } string[] trackNames = header.GetTrackNames(); if (trackNames.Length > 1 && trackNames.Length > currentTrack) { label += Environment.NewLine + (string.IsNullOrWhiteSpace(trackNames[currentTrack]) ? ResourceHelper.GetMessage("NsfUnnamedTrack") : trackNames[currentTrack]); } bool rewinding = InteropEmu.IsRewinding(); lblRecording.Visible = lblRecordingDot.Visible = InteropEmu.WaveIsRecording(); lblRewinding.Visible = lblRewindIcon.Visible = rewinding; lblFastForward.Visible = lblFastForwardIcon.Visible = (InteropEmu.GetEmulationSpeed() > 100 || InteropEmu.GetEmulationSpeed() == 0 || InteropEmu.CheckFlag(EmulationFlags.Turbo)) && !rewinding; lblSlowMotion.Visible = lblSlowMotionIcon.Visible = InteropEmu.GetEmulationSpeed() < 100 && InteropEmu.GetEmulationSpeed() > 0 && !InteropEmu.CheckFlag(EmulationFlags.Turbo) && !rewinding; lblTime.Text = label; trkVolume.Value = (int)ConfigManager.Config.AudioInfo.MasterVolume; }
public void UpdateText() { if (this.InvokeRequired) { this.BeginInvoke((MethodInvoker)(() => UpdateText())); } else { _isNsf = InteropEmu.IsNsf(); if (_isNsf) { UpdateTrackDisplay(); UpdateTimeDisplay(); toolTip.SetToolTip(btnNext, ResourceHelper.GetMessage("NsfNextTrack")); NsfHeader header = InteropEmu.NsfGetHeader(); this.UpdateVolume(); lblTitleValue.Text = header.GetSongName(); lblArtistValue.Text = header.GetArtistName(); lblCopyrightValue.Text = header.GetCopyrightHolder(); lblVrc6.ForeColor = (header.SoundChips & 0x01) == 0x01 ? Color.White : Color.Gray; lblVrc7.ForeColor = (header.SoundChips & 0x02) == 0x02 ? Color.White : Color.Gray; lblFds.ForeColor = (header.SoundChips & 0x04) == 0x04 ? Color.White : Color.Gray; lblMmc5.ForeColor = (header.SoundChips & 0x08) == 0x08 ? Color.White : Color.Gray; lblNamco.ForeColor = (header.SoundChips & 0x10) == 0x10 ? Color.White : Color.Gray; lblSunsoft.ForeColor = (header.SoundChips & 0x20) == 0x20 ? Color.White : Color.Gray; lblEpsg.ForeColor = (header.SoundChips & 0x40) == 0x40 ? Color.White : Color.Gray; if (InteropEmu.IsPaused()) { btnPause.Image = Properties.Resources.Play; } else { btnPause.Image = Properties.Resources.Pause; } } } }
private void UpdateVectorAddresses() { RomFormat format = InteropEmu.GetRomInfo().Format; if (format == RomFormat.Nsf) { NsfHeader header = InteropEmu.NsfGetHeader(); mnuGoToInitHandler.ShortcutKeyDisplayString = "$" + header.InitAddress.ToString("X4"); mnuGoToPlayHandler.ShortcutKeyDisplayString = "$" + header.PlayAddress.ToString("X4"); } else { mnuGoToNmiHandler.ShortcutKeyDisplayString = "$" + GetHandlerTarget(0xFFFA).ToString("X4"); mnuGoToResetHandler.ShortcutKeyDisplayString = "$" + GetHandlerTarget(0xFFFC).ToString("X4"); mnuGoToIrqHandler.ShortcutKeyDisplayString = "$" + GetHandlerTarget(0xFFFE).ToString("X4"); if (format == RomFormat.Fds) { mnuFdsIrqHandler.ShortcutKeyDisplayString = "$" + GetHandlerTarget(0xDFFE).ToString("X4"); mnuFdsNmiHandler1.ShortcutKeyDisplayString = "$" + GetHandlerTarget(0xDFF6).ToString("X4"); mnuFdsNmiHandler2.ShortcutKeyDisplayString = "$" + GetHandlerTarget(0xDFF8).ToString("X4"); mnuFdsNmiHandler3.ShortcutKeyDisplayString = "$" + GetHandlerTarget(0xDFFA).ToString("X4"); mnuFdsResetHandler.ShortcutKeyDisplayString = "$" + GetHandlerTarget(0xDFFC).ToString("X4"); } } mnuGoToInitHandler.Tag = mnuGoToInitHandler.Visible = format == RomFormat.Nsf; mnuGoToPlayHandler.Tag = mnuGoToPlayHandler.Visible = format == RomFormat.Nsf; mnuGoToIrqHandler.Tag = mnuGoToIrqHandler.Visible = format != RomFormat.Nsf; mnuGoToNmiHandler.Tag = mnuGoToNmiHandler.Visible = format != RomFormat.Nsf; mnuGoToResetHandler.Tag = mnuGoToResetHandler.Visible = format != RomFormat.Nsf; sepFds.Tag = sepFds.Visible = format == RomFormat.Fds; mnuFdsIrqHandler.Tag = mnuFdsIrqHandler.Visible = format == RomFormat.Fds; mnuFdsNmiHandler1.Tag = mnuFdsNmiHandler1.Visible = format == RomFormat.Fds; mnuFdsNmiHandler2.Tag = mnuFdsNmiHandler2.Visible = format == RomFormat.Fds; mnuFdsNmiHandler3.Tag = mnuFdsNmiHandler3.Visible = format == RomFormat.Fds; mnuFdsResetHandler.Tag = mnuFdsResetHandler.Visible = format == RomFormat.Fds; }
private TimeSpan GetTrackLength(NsfHeader header, int track) { int trackLength = header.TrackLength[track]; if (header.TotalSongs > 1 && trackLength < 0 && ConfigManager.Config.PreferenceInfo.NsfMoveToNextTrackAfterTime) { trackLength = (ConfigManager.Config.PreferenceInfo.NsfMoveToNextTrackTime - 1) * 1000; } if (trackLength >= 0) { int trackFade = header.TrackFade[track]; if (trackFade < 0) { //1 sec by default trackFade = 1000; } trackLength += trackFade; return(TimeSpan.FromSeconds((double)trackLength / 1000)); } return(TimeSpan.FromSeconds(0)); }
public unsafe static bool Save(Project originalProject, 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); // Code/sound engine var nsfBinStream = typeof(NsfFile).Assembly.GetManifestResourceStream("FamiStudio.Nsf.nsf.bin"); 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(); 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(); 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, int kernel, string filename, int[] songIds, string name, string author, string copyright, int machine) { try { 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."); } 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 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); }