Exemplo n.º 1
0
        private void SetupProject(Project originalProject, int[] songIds)
        {
            // Work on a temporary copy.
            project          = originalProject.DeepClone();
            project.Filename = originalProject.Filename;

            if (kernel == FamiToneKernel.FamiTone2 && project.UsesFamiStudioTempo)
            {
                project.ConvertToFamiTrackerTempo(false);
            }

            // NULL = All songs.
            if (songIds != null)
            {
                for (int i = 0; i < project.Songs.Count; i++)
                {
                    if (!songIds.Contains(project.Songs[i].Id))
                    {
                        project.DeleteSong(project.Songs[i]);
                        i--;
                    }
                }
            }

            RemoveUnsupportedFeatures();
            project.DeleteUnusedInstruments();
        }
Exemplo n.º 2
0
        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);
        }
Exemplo n.º 3
0
        public unsafe bool Save(Project originalProject, string filename, int[] songIds, string name, string author, bool pal)
        {
            try
            {
                if (songIds.Length == 0)
                {
                    return(false);
                }

                Debug.Assert(!originalProject.UsesExpansionAudio || !pal);

                if (songIds.Length > MaxSongs)
                {
                    Array.Resize(ref songIds, MaxSongs);
                }

                var project = originalProject.DeepClone();
                project.RemoveAllSongsBut(songIds);
                project.SetExpansionAudio(Project.ExpansionNone);

                var headerBytes = new byte[RomHeaderLength];
                var codeBytes   = new byte[RomCodeSize + RomTileSize];

                // Load ROM header (16 bytes) + code/tiles (12KB).
                string romName = "FamiStudio.Rom.rom";
                if (project.UsesFamiTrackerTempo)
                {
                    romName += "_famitracker";
                }
                romName += pal ? "_pal" : "_ntsc";
                romName += ".nes";

                var romBinStream = typeof(RomFile).Assembly.GetManifestResourceStream(romName);
                romBinStream.Read(headerBytes, 0, RomHeaderLength);
                romBinStream.Seek(-RomCodeSize - RomTileSize, SeekOrigin.End);
                romBinStream.Read(codeBytes, 0, RomCodeSize + RomTileSize);

                Log.LogMessage(LogSeverity.Info, $"ROM code and graphics size: {codeBytes.Length} bytes.");

                // Build project info + song table of content.
                var projectInfo = BuildProjectInfo(songIds, name, author);
                var songTable   = BuildSongTableOfContent(project);

                // Gather DPCM + song data.
                var songDataBytes = new List <byte>();
                var dpcmBaseAddr  = RomDpcmOffset;

                // We will put samples right at the beginning.
                if (project.UsesSamples)
                {
                    // Since we keep the code/engine at f000 all the time, we are limited to 12KB of samples in ROM.
                    var totalSampleSize = project.GetTotalSampleSize();
                    var dpcmPageCount   = Math.Min(MaxDpcmPages, (totalSampleSize + (RomPageSize - 1)) / RomPageSize);

                    // Otherwise we will allocate at least a full page for the samples and use the following mapping:
                    //    0KB -  4KB samples: starts at 0xe000
                    //    4KB -  8KB samples: starts at 0xd000
                    //    8KB - 12KB samples: starts at 0xc000
                    dpcmBaseAddr += (MaxDpcmPages - dpcmPageCount) * RomPageSize;

                    var dpcmBytes = project.GetPackedSampleData();
                    if (dpcmBytes.Length > (MaxDpcmPages * RomPageSize))
                    {
                        Log.LogMessage(LogSeverity.Warning, $"DPCM samples size ({dpcmBytes.Length}) is larger than the maximum allowed for ROM export ({MaxDpcmPages * RomPageSize}). Truncating.");
                        Array.Resize(ref dpcmBytes, MaxDpcmPages * RomPageSize);
                    }

                    songDataBytes.AddRange(dpcmBytes);

                    projectInfo.dpcmPageCount = (byte)dpcmPageCount;
                    projectInfo.dpcmPageStart = (byte)0;

                    Log.LogMessage(LogSeverity.Info, $"DPCM allocated size: {dpcmPageCount * RomPageSize} 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];
                    int page      = songDataBytes.Count / RomPageSize;
                    int addr      = RomMemoryStart + (songDataBytes.Count & (RomPageSize - 1));
                    var songBytes = new FamitoneMusicFile(FamitoneMusicFile.FamiToneKernel.FamiStudio, false).GetBytes(project, new int[] { song.Id }, addr, dpcmBaseAddr, pal ? MachineType.PAL : MachineType.NTSC);

                    songTable[i].page    = (byte)(page);
                    songTable[i].address = (ushort)(addr);
                    songTable[i].flags   = (byte)(song.UsesDpcm ? 1 : 0);

                    songDataBytes.AddRange(songBytes);

                    Log.LogMessage(LogSeverity.Info, $"Song '{song.Name}' size: {songBytes.Length} bytes.");
                }

                //File.WriteAllBytes("D:\\debug.bin", songDataBytes.ToArray());

                int numPrgBanks = RomMinSize / RomPrgBankSize;

                if (songDataBytes.Count > (RomMinSize - RomCodeSize))
                {
                    numPrgBanks = (songDataBytes.Count + (RomPrgBankSize - 1)) / RomPrgBankSize;
                }

                int padding = (numPrgBanks * RomPrgBankSize) - RomCodeSize - songDataBytes.Count;
                songDataBytes.AddRange(new byte[padding]);

                // Patch in code (project info and song table are at the beginning, 0xf000).
                Marshal.Copy(new IntPtr(&projectInfo), codeBytes, 0, sizeof(RomProjectInfo));

                for (int i = 0; i < MaxSongs; i++)
                {
                    fixed(RomSongEntry *songEntry = &songTable[i])
                    Marshal.Copy(new IntPtr(songEntry), codeBytes, sizeof(RomProjectInfo) + i * sizeof(RomSongEntry), sizeof(RomSongEntry));
                }

                // Patch header.
                headerBytes[RomHeaderPrgOffset] = (byte)numPrgBanks;

                // Build final ROM and save.
                var romBytes = new List <byte>();
                romBytes.AddRange(headerBytes);
                romBytes.AddRange(songDataBytes);
                romBytes.AddRange(codeBytes);

                File.WriteAllBytes(filename, romBytes.ToArray());

                Log.LogMessage(LogSeverity.Info, $"ROM export successful, final file size {romBytes.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);
        }
Exemplo n.º 4
0
        public unsafe bool Save(Project originalProject, int songId, int loopCount, string filename, int resX, int resY, bool halfFrameRate, int channelMask, int audioBitRate, int videoBitRate, float pianoRollZoom, bool stereo, float[] pan)
        {
            if (!Initialize(channelMask, loopCount))
            {
                return(false);
            }

            videoResX = resX;
            videoResY = resY;

            var project = originalProject.DeepClone();
            var song    = project.GetSong(songId);

            ExtendSongForLooping(song, loopCount);

            // Save audio to temporary file.
            Log.LogMessage(LogSeverity.Info, "Exporting audio...");

            var tempFolder    = Utils.GetTemporaryDiretory();
            var tempAudioFile = Path.Combine(tempFolder, "temp.wav");

            AudioExportUtils.Save(song, tempAudioFile, SampleRate, 1, -1, channelMask, false, false, stereo, pan, (samples, samplesChannels, fn) => { WaveFile.Save(samples, fn, SampleRate, samplesChannels); });

            // Start encoder, must be done before any GL calls on Android.
            GetFrameRateInfo(song.Project, halfFrameRate, out var frameRateNumer, out var frameRateDenom);

            if (!videoEncoder.BeginEncoding(videoResX, videoResY, frameRateNumer, frameRateDenom, videoBitRate, audioBitRate, tempAudioFile, filename))
            {
                Log.LogMessage(LogSeverity.Error, "Error starting video encoder, aborting.");
                return(false);
            }

            Log.LogMessage(LogSeverity.Info, "Initializing channels...");

            var numChannels        = Utils.NumberOfSetBits(channelMask);
            var channelResXFloat   = videoResX / (float)numChannels;
            var channelResX        = videoResY;
            var channelResY        = (int)channelResXFloat;
            var longestChannelName = 0.0f;

            var videoGraphics = RenderGraphics.Create(videoResX, videoResY, true);

            if (videoGraphics == null)
            {
                Log.LogMessage(LogSeverity.Error, "Error initializing off-screen graphics, aborting.");
                return(false);
            }

            var themeResources = new ThemeRenderResources(videoGraphics);
            var bmpWatermark   = videoGraphics.CreateBitmapFromResource("VideoWatermark");

            // Generate WAV data for each individual channel for the oscilloscope.
            var channelStates = new List <VideoChannelState>();
            var maxAbsSample  = 0;

            for (int i = 0, channelIndex = 0; i < song.Channels.Length; i++)
            {
                if ((channelMask & (1 << i)) == 0)
                {
                    continue;
                }

                var pattern = song.Channels[i].PatternInstances[0];
                var state   = new VideoChannelState();

                state.videoChannelIndex = channelIndex;
                state.songChannelIndex  = i;
                state.channel           = song.Channels[i];
                state.channelText       = state.channel.NameWithExpansion;
                state.wav      = new WavPlayer(SampleRate, 1, 1 << i).GetSongSamples(song, song.Project.PalMode, -1);
                state.graphics = RenderGraphics.Create(channelResX, channelResY, false);
                state.bitmap   = videoGraphics.CreateBitmapFromOffscreenGraphics(state.graphics);

                channelStates.Add(state);
                channelIndex++;

                // Find maximum absolute value to rescale the waveform.
                foreach (int s in state.wav)
                {
                    maxAbsSample = Math.Max(maxAbsSample, Math.Abs(s));
                }

                // Measure the longest text.
                longestChannelName = Math.Max(longestChannelName, state.graphics.MeasureString(state.channelText, themeResources.FontVeryLarge));

                Log.ReportProgress(0.0f);
            }

            // Tweak some cosmetic stuff that depends on resolution.
            var smallChannelText = longestChannelName + 32 + ChannelIconTextSpacing > channelResY * 0.8f;
            var bmpSuffix        = smallChannelText ? "" : "@2x";
            var font             = smallChannelText ? themeResources.FontMedium : themeResources.FontVeryLarge;
            var textOffsetY      = smallChannelText ? 1 : 4;
            var pianoRollScaleX  = Utils.Clamp(resY / 1080.0f, 0.6f, 0.9f);
            var pianoRollScaleY  = channelResY < VeryThinNoteThreshold ? 0.5f : (channelResY < ThinNoteThreshold ? 0.667f : 1.0f);
            var channelLineWidth = channelResY < ThinNoteThreshold ? 3 : 5;
            var gradientSizeY    = 256 * (videoResY / 1080.0f);
            var gradientBrush    = videoGraphics.CreateVerticalGradientBrush(0, gradientSizeY, Color.Black, Color.FromArgb(0, Color.Black));

            foreach (var s in channelStates)
            {
                s.bmpIcon = videoGraphics.CreateBitmapFromResource(ChannelType.Icons[s.channel.Type] + bmpSuffix);
            }

            // Generate the metadata for the video so we know what's happening at every frame
            var metadata = new VideoMetadataPlayer(SampleRate, 1).GetVideoMetadata(song, song.Project.PalMode, -1);

            var oscScale    = maxAbsSample != 0 ? short.MaxValue / (float)maxAbsSample : 1.0f;
            var oscLookback = (metadata[1].wavOffset - metadata[0].wavOffset) / 2;

            // Setup piano roll and images.
            var pianoRoll = new PianoRoll();

            pianoRoll.Move(0, 0, channelResX, channelResY);
            pianoRoll.SetThemeRenderResource(themeResources);
            pianoRoll.StartVideoRecording(channelStates[0].graphics, song, pianoRollZoom, pianoRollScaleX, pianoRollScaleY, out var noteSizeY);

            // Build the scrolling data.
            var numVisibleNotes = (int)Math.Floor(channelResY / (float)noteSizeY);

            ComputeChannelsScroll(metadata, channelMask, numVisibleNotes);

            if (song.UsesFamiTrackerTempo)
            {
                SmoothFamitrackerScrolling(metadata);
            }
            else
            {
                SmoothFamiStudioScrolling(metadata, song);
            }

            var oscWindowSize = (int)Math.Round(SampleRate * OscilloscopeWindowSize);

            var videoImage   = new byte[videoResY * videoResX * 4];
            var oscilloscope = new float[oscWindowSize, 2];
            var success      = true;

#if !DEBUG
            try
#endif
            {
                // Generate each of the video frames.
                for (int f = 0; f < metadata.Length; f++)
                {
                    if (Log.ShouldAbortOperation)
                    {
                        success = false;
                        break;
                    }

                    if ((f % 100) == 0)
                    {
                        Log.LogMessage(LogSeverity.Info, $"Rendering frame {f} / {metadata.Length}");
                    }

                    Log.ReportProgress(f / (float)(metadata.Length - 1));

                    if (halfFrameRate && (f & 1) != 0)
                    {
                        continue;
                    }

                    var frame = metadata[f];

                    // Render the piano rolls for each channels.
                    foreach (var s in channelStates)
                    {
                        var volume = frame.channelVolumes[s.songChannelIndex];
                        var note   = frame.channelNotes[s.songChannelIndex];

                        var color = Color.Transparent;

                        if (note.IsMusical)
                        {
                            if (s.channel.Type == ChannelType.Dpcm)
                            {
                                var mapping = project.GetDPCMMapping(note.Value);
                                if (mapping != null)
                                {
                                    color = mapping.Sample.Color;
                                }
                            }
                            else
                            {
                                color = Color.FromArgb(128 + volume * 127 / 15, note.Instrument != null ? note.Instrument.Color : Theme.LightGreyFillColor1);
                            }
                        }

                        s.graphics.BeginDrawFrame();
                        s.graphics.BeginDrawControl(new Rectangle(0, 0, channelResX, channelResY), channelResY);
                        pianoRoll.RenderVideoFrame(s.graphics, s.channel.Index, frame.playPattern, frame.playNote, frame.scroll[s.songChannelIndex], note.Value, color);
                        s.graphics.EndDrawControl();
                        s.graphics.EndDrawFrame();
                    }

                    // Render the full screen overlay.
                    videoGraphics.BeginDrawFrame();
                    videoGraphics.BeginDrawControl(new Rectangle(0, 0, videoResX, videoResY), videoResY);
                    videoGraphics.Clear(Color.Black);

                    var bg = videoGraphics.CreateCommandList();
                    var fg = videoGraphics.CreateCommandList();

                    // Composite the channel renders.
                    foreach (var s in channelStates)
                    {
                        int channelPosX0 = (int)Math.Round(s.videoChannelIndex * channelResXFloat);
                        bg.DrawBitmap(s.bitmap, channelPosX0, 0, s.bitmap.Size.Height, s.bitmap.Size.Width, 1.0f, 0, 0, 1, 1, true);
                    }

                    // Gradient
                    fg.FillRectangle(0, 0, videoResX, gradientSizeY, gradientBrush);

                    // Channel names + oscilloscope
                    foreach (var s in channelStates)
                    {
                        int channelPosX0 = (int)Math.Round((s.videoChannelIndex + 0) * channelResXFloat);
                        int channelPosX1 = (int)Math.Round((s.videoChannelIndex + 1) * channelResXFloat);

                        var channelNameSizeX = (int)videoGraphics.MeasureString(s.channelText, font);
                        var channelIconPosX  = channelPosX0 + channelResY / 2 - (channelNameSizeX + s.bmpIcon.Size.Width + ChannelIconTextSpacing) / 2;

                        fg.FillAndDrawRectangle(channelIconPosX, ChannelIconPosY, channelIconPosX + s.bmpIcon.Size.Width - 1, ChannelIconPosY + s.bmpIcon.Size.Height - 1, themeResources.DarkGreyLineBrush2, themeResources.LightGreyFillBrush1);
                        fg.DrawBitmap(s.bmpIcon, channelIconPosX, ChannelIconPosY, 1, Theme.LightGreyFillColor1);
                        fg.DrawText(s.channelText, font, channelIconPosX + s.bmpIcon.Size.Width + ChannelIconTextSpacing, ChannelIconPosY + textOffsetY, themeResources.LightGreyFillBrush1);

                        if (s.videoChannelIndex > 0)
                        {
                            fg.DrawLine(channelPosX0, 0, channelPosX0, videoResY, themeResources.BlackBrush, channelLineWidth);
                        }

                        var oscMinY = (int)(ChannelIconPosY + s.bmpIcon.Size.Height + 10);
                        var oscMaxY = (int)(oscMinY + 100.0f * (resY / 1080.0f));

                        // Intentionally flipping min/max Y since D3D is upside down compared to how we display waves typically.
                        GenerateOscilloscope(s.wav, frame.wavOffset, oscWindowSize, oscLookback, oscScale, channelPosX0 + 10, oscMaxY, channelPosX1 - 10, oscMinY, oscilloscope);

                        fg.DrawGeometry(oscilloscope, themeResources.LightGreyFillBrush1, 1, true);
                    }

                    // Watermark.
                    fg.DrawBitmap(bmpWatermark, videoResX - bmpWatermark.Size.Width, videoResY - bmpWatermark.Size.Height);

                    videoGraphics.DrawCommandList(bg);
                    videoGraphics.DrawCommandList(fg);
                    videoGraphics.EndDrawControl();
                    videoGraphics.EndDrawFrame();

                    // Readback
                    videoGraphics.GetBitmap(videoImage);

                    // Send to encoder.
                    videoEncoder.AddFrame(videoImage);

                    // Dump debug images.
                    // DumpDebugImage(videoImage, videoResX, videoResY, f);
                }

                videoEncoder.EndEncoding(!success);

                File.Delete(tempAudioFile);
            }
#if !DEBUG
            catch (Exception e)
            {
                Log.LogMessage(LogSeverity.Error, "Error exporting video.");
                Log.LogMessage(LogSeverity.Error, e.Message);
            }
            finally
#endif
            {
                pianoRoll.EndVideoRecording();
                foreach (var c in channelStates)
                {
                    c.bmpIcon.Dispose();
                    c.bitmap.Dispose();
                    c.graphics.Dispose();
                }
                themeResources.Dispose();
                bmpWatermark.Dispose();
                gradientBrush.Dispose();
                videoGraphics.Dispose();
            }

            return(true);
        }
Exemplo n.º 5
0
        public unsafe bool Save(Project originalProject, string filename, int[] songIds, string name, string author)
        {
            try
            {
                if (songIds.Length == 0)
                {
                    return(false);
                }

                if (songIds.Length > MaxSongs)
                {
                    Array.Resize(ref songIds, MaxSongs);
                }

                var project = originalProject.DeepClone();
                project.RemoveAllSongsBut(songIds);

                if (project.ExpansionAudio != ExpansionType.Fds)
                {
                    project.SetExpansionAudio(ExpansionType.Fds);
                }

                string fdsDiskName = "FamiStudio.Rom.fds";
                if (project.UsesFamiTrackerTempo)
                {
                    fdsDiskName += "_famitracker";
                }
                fdsDiskName += ".fds";

                // Read FDS disk header + code.
                var fdsDiskBinStream = typeof(RomFile).Assembly.GetManifestResourceStream(fdsDiskName);
                var fdsDiskInitBytes = new byte[fdsDiskBinStream.Length];
                fdsDiskBinStream.Read(fdsDiskInitBytes, 0, fdsDiskInitBytes.Length);

                TruncateToLastFile(ref fdsDiskInitBytes);

                var fdsFileBytes = new List <byte>();
                fdsFileBytes.AddRange(fdsDiskInitBytes);

                Log.LogMessage(LogSeverity.Info, $"FDS code and graphics files: {fdsDiskInitBytes.Length} bytes.");

                var fileIndex     = FdsFirstFileIndex;
                var dpcmFileIndex = 0;

                // Create the DPCM file if needed.
                if (project.UsesSamples)
                {
                    var dpcmBytes = project.GetPackedSampleData();

                    if (dpcmBytes.Length > FdsMaxDpcmSize)
                    {
                        Log.LogMessage(LogSeverity.Warning, $"DPCM samples size ({dpcmBytes.Length}) is larger than the maximum allowed for FDS export ({FdsMaxDpcmSize}). Truncating.");
                        Array.Resize(ref dpcmBytes, FdsMaxDpcmSize);
                    }

                    AddFile(fdsFileBytes, fileIndex, FdsDpcmStart, "DPCM....", dpcmBytes);

                    dpcmFileIndex = fileIndex;
                    fileIndex++;

                    Log.LogMessage(LogSeverity.Info, $"DPCM file size: {dpcmBytes.Length} bytes.");
                }

                var projectInfo = BuildProjectInfo(songIds, name, author);
                var songTable   = BuildSongTableOfContent(project);

                // Export each song as an individual file.
                for (int i = 0; i < project.Songs.Count; i++)
                {
                    var song      = project.Songs[i];
                    var songBytes = new FamitoneMusicFile(FamiToneKernel.FamiStudio, false).GetBytes(project, new int[] { song.Id }, FdsSongDataAddr, FdsDpcmStart, MachineType.NTSC);

                    songTable[i].page  = (byte)fileIndex;
                    songTable[i].flags = (byte)(song.UsesDpcm ? dpcmFileIndex : 0);

                    if (songBytes.Length > FdsMaxSongSize)
                    {
                        Log.LogMessage(LogSeverity.Warning, $"Song '{song.Name}' is too large ({songBytes.Length} bytes, maximum is {FdsMaxSongSize}). File will be corrupted.");
                        Array.Resize(ref songBytes, FdsMaxSongSize);
                    }

                    if (fdsFileBytes.Count + FdsBlockHeaderSize + songBytes.Length > FdsMaxFileSize)
                    {
                        Log.LogMessage(LogSeverity.Warning, $"Reached maximum file size ({FdsMaxFileSize}). Songs will be missing.");
                        break;
                    }

                    AddFile(fdsFileBytes, fileIndex, FdsSongDataAddr, $"SONG{i}...", songBytes);

                    fileIndex++;

                    Log.LogMessage(LogSeverity.Info, $"Song '{song.Name}' file size: {songBytes.Length} bytes.");
                }

                //File.WriteAllBytes("D:\\dump\\fdsdata.bin", fdsFileBytes.ToArray());

                // Use this field for the number of files.
                projectInfo.dpcmPageCount = (byte)fileIndex;

                // Pad rest with zeroes.
                fdsFileBytes.AddRange(new byte[FdsMaxFileSize - fdsFileBytes.Count]);

                // Build project info + song table of content.
                var tocBytes = new byte[sizeof(RomProjectInfo) + sizeof(RomSongEntry) * songTable.Length];

                Marshal.Copy(new IntPtr(&projectInfo), tocBytes, 0, sizeof(RomProjectInfo));

                for (int i = 0; i < MaxSongs; i++)
                {
                    fixed(RomSongEntry *songEntry = &songTable[i])
                    Marshal.Copy(new IntPtr(songEntry), tocBytes, sizeof(RomProjectInfo) + i * sizeof(RomSongEntry), sizeof(RomSongEntry));
                }

                //File.WriteAllBytes("D:\\dump\\fdstoc.bin", tocBytes);

                // Path TOC file.
                var byteArray = fdsFileBytes.ToArray();
                PatchFile(byteArray, "TOC.....", tocBytes);

                // Build final ROM and save.
                File.WriteAllBytes(filename, byteArray);

                Log.LogMessage(LogSeverity.Info, $"FDS export successful, final file size {byteArray.Length} 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);
        }
Exemplo n.º 6
0
        public unsafe bool Save(Project originalProject, int songId, int loopCount, string ffmpegExecutable, string filename, int resX, int resY, bool halfFrameRate, int channelMask, int audioBitRate, int videoBitRate, int pianoRollZoom)
        {
            if (channelMask == 0 || loopCount < 1)
            {
                return(false);
            }

            Log.LogMessage(LogSeverity.Info, "Detecting FFmpeg...");

            if (!DetectFFmpeg(ffmpegExecutable))
            {
                return(false);
            }

            videoResX = resX;
            videoResY = resY;

            var project = originalProject.DeepClone();
            var song    = project.GetSong(songId);

            ExtendSongForLooping(song, loopCount);

            Log.LogMessage(LogSeverity.Info, "Initializing channels...");

            var frameRateNumerator = song.Project.PalMode ? 5000773 : 6009883;

            if (halfFrameRate)
            {
                frameRateNumerator /= 2;
            }
            var frameRate = frameRateNumerator.ToString() + "/100000";

            var numChannels        = Utils.NumberOfSetBits(channelMask);
            var channelResXFloat   = videoResX / (float)numChannels;
            var channelResX        = videoResY;
            var channelResY        = (int)channelResXFloat;
            var longestChannelName = 0.0f;

            var videoGraphics = RenderGraphics.Create(videoResX, videoResY, true);

            if (videoGraphics == null)
            {
                Log.LogMessage(LogSeverity.Error, "Error initializing off-screen graphics, aborting.");
                return(false);
            }

            var theme        = RenderTheme.CreateResourcesForGraphics(videoGraphics);
            var bmpWatermark = videoGraphics.CreateBitmapFromResource("VideoWatermark");

            // Generate WAV data for each individual channel for the oscilloscope.
            var channelStates = new List <VideoChannelState>();

            List <short[]> channelsWavData = new List <short[]>();
            var            maxAbsSample    = 0;

            for (int i = 0, channelIndex = 0; i < song.Channels.Length; i++)
            {
                if ((channelMask & (1 << i)) == 0)
                {
                    continue;
                }

                var pattern = song.Channels[i].PatternInstances[0];
                var state   = new VideoChannelState();

                state.videoChannelIndex = channelIndex;
                state.songChannelIndex  = i;
                state.channel           = song.Channels[i];
                state.patternIndex      = 0;
                state.channelText       = state.channel.Name + (state.channel.IsExpansionChannel ? $" ({song.Project.ExpansionAudioShortName})" : "");
                state.wav      = new WavPlayer(SampleRate, 1, 1 << i).GetSongSamples(song, song.Project.PalMode, -1);
                state.graphics = RenderGraphics.Create(channelResX, channelResY, false);
                state.bitmap   = videoGraphics.CreateBitmapFromOffscreenGraphics(state.graphics);

                channelStates.Add(state);
                channelIndex++;

                // Find maximum absolute value to rescale the waveform.
                foreach (short s in state.wav)
                {
                    maxAbsSample = Math.Max(maxAbsSample, Math.Abs(s));
                }

                // Measure the longest text.
                longestChannelName = Math.Max(longestChannelName, state.graphics.MeasureString(state.channelText, ThemeBase.FontBigUnscaled));
            }

            // Tweak some cosmetic stuff that depends on resolution.
            var smallChannelText = longestChannelName + 32 + ChannelIconTextSpacing > channelResY * 0.8f;
            var bmpSuffix        = smallChannelText ? "" : "@2x";
            var font             = smallChannelText ? ThemeBase.FontMediumUnscaled : ThemeBase.FontBigUnscaled;
            var textOffsetY      = smallChannelText ? 1 : 4;
            var pianoRollScaleX  = Utils.Clamp(resY / 1080.0f, 0.6f, 0.9f);
            var pianoRollScaleY  = channelResY < VeryThinNoteThreshold ? 0.5f : (channelResY < ThinNoteThreshold ? 0.667f : 1.0f);
            var channelLineWidth = channelResY < ThinNoteThreshold ? 3 : 5;
            var gradientSizeY    = 256 * (videoResY / 1080.0f);
            var gradientBrush    = videoGraphics.CreateVerticalGradientBrush(0, gradientSizeY, Color.Black, Color.FromArgb(0, Color.Black));

            foreach (var s in channelStates)
            {
                s.bmpIcon = videoGraphics.CreateBitmapFromResource(ChannelType.Icons[s.channel.Type] + bmpSuffix);
            }

            // Generate the metadata for the video so we know what's happening at every frame
            var metadata = new VideoMetadataPlayer(SampleRate, 1).GetVideoMetadata(song, song.Project.PalMode, -1);

            var oscScale    = maxAbsSample != 0 ? short.MaxValue / (float)maxAbsSample : 1.0f;
            var oscLookback = (metadata[1].wavOffset - metadata[0].wavOffset) / 2;

#if FAMISTUDIO_LINUX || FAMISTUDIO_MACOS
            var dummyControl = new DummyGLControl();
            dummyControl.Move(0, 0, videoResX, videoResY);
#endif

            // Setup piano roll and images.
            var pianoRoll = new PianoRoll();
#if FAMISTUDIO_LINUX || FAMISTUDIO_MACOS
            pianoRoll.Move(0, 0, channelResX, channelResY);
#else
            pianoRoll.Width  = channelResX;
            pianoRoll.Height = channelResY;
#endif

            pianoRoll.StartVideoRecording(channelStates[0].graphics, song, pianoRollZoom, pianoRollScaleX, pianoRollScaleY, out var noteSizeY);

            // Build the scrolling data.
            var numVisibleNotes = (int)Math.Floor(channelResY / (float)noteSizeY);
            ComputeChannelsScroll(metadata, channelMask, numVisibleNotes);

            if (song.UsesFamiTrackerTempo)
            {
                SmoothFamitrackerScrolling(metadata);
            }
            else
            {
                SmoothFamiStudioScrolling(metadata, song);
            }

            var videoImage   = new byte[videoResY * videoResX * 4];
            var oscilloscope = new float[channelResY, 2];

            // Start ffmpeg with pipe input.
            var tempFolder    = Utils.GetTemporaryDiretory();
            var tempAudioFile = Path.Combine(tempFolder, "temp.wav");

#if !DEBUG
            try
#endif
            {
                Log.LogMessage(LogSeverity.Info, "Exporting audio...");

                // Save audio to temporary file.
                WavMp3ExportUtils.Save(song, tempAudioFile, SampleRate, 1, -1, channelMask, false, false, (samples, fn) => { WaveFile.Save(samples, fn, SampleRate); });

                var process = LaunchFFmpeg(ffmpegExecutable, $"-y -f rawvideo -pix_fmt argb -s {videoResX}x{videoResY} -r {frameRate} -i - -i \"{tempAudioFile}\" -c:v h264 -pix_fmt yuv420p -b:v {videoBitRate}K -c:a aac -b:a {audioBitRate}k \"{filename}\"", true, false);

                // Generate each of the video frames.
                using (var stream = new BinaryWriter(process.StandardInput.BaseStream))
                {
                    for (int f = 0; f < metadata.Length; f++)
                    {
                        if (Log.ShouldAbortOperation)
                        {
                            break;
                        }

                        if ((f % 100) == 0)
                        {
                            Log.LogMessage(LogSeverity.Info, $"Rendering frame {f} / {metadata.Length}");
                        }

                        Log.ReportProgress(f / (float)(metadata.Length - 1));

                        if (halfFrameRate && (f & 1) != 0)
                        {
                            continue;
                        }

                        var frame = metadata[f];

                        // Render the piano rolls for each channels.
                        foreach (var s in channelStates)
                        {
                            s.volume = frame.channelVolumes[s.songChannelIndex];
                            s.note   = frame.channelNotes[s.songChannelIndex];

                            var color = Color.Transparent;

                            if (s.note.IsMusical)
                            {
                                if (s.channel.Type == ChannelType.Dpcm)
                                {
                                    var mapping = project.GetDPCMMapping(s.note.Value);
                                    if (mapping != null && mapping.Sample != null)
                                    {
                                        color = mapping.Sample.Color;
                                    }
                                }
                                else
                                {
                                    color = Color.FromArgb(128 + s.volume * 127 / 15, s.note.Instrument != null ? s.note.Instrument.Color : ThemeBase.DarkGreyFillColor2);
                                }
                            }

#if FAMISTUDIO_LINUX || FAMISTUDIO_MACOS
                            s.graphics.BeginDraw(pianoRoll, channelResY);
#else
                            s.graphics.BeginDraw();
#endif
                            pianoRoll.RenderVideoFrame(s.graphics, Channel.ChannelTypeToIndex(s.channel.Type), frame.playPattern, frame.playNote, frame.scroll[s.songChannelIndex], s.note.Value, color);
                            s.graphics.EndDraw();
                        }

                        // Render the full screen overlay.
#if FAMISTUDIO_LINUX || FAMISTUDIO_MACOS
                        videoGraphics.BeginDraw(dummyControl, videoResY);
#else
                        videoGraphics.BeginDraw();
#endif
                        videoGraphics.Clear(Color.Black);

                        // Composite the channel renders.
                        foreach (var s in channelStates)
                        {
                            int channelPosX1 = (int)Math.Round((s.videoChannelIndex + 1) * channelResXFloat);
                            videoGraphics.DrawRotatedFlippedBitmap(s.bitmap, channelPosX1, videoResY, s.bitmap.Size.Width, s.bitmap.Size.Height);
                        }

                        // Gradient
                        videoGraphics.FillRectangle(0, 0, videoResX, gradientSizeY, gradientBrush);

                        // Channel names + oscilloscope
                        foreach (var s in channelStates)
                        {
                            int channelPosX0 = (int)Math.Round((s.videoChannelIndex + 0) * channelResXFloat);
                            int channelPosX1 = (int)Math.Round((s.videoChannelIndex + 1) * channelResXFloat);

                            var channelNameSizeX = videoGraphics.MeasureString(s.channelText, font);
                            var channelIconPosX  = channelPosX0 + channelResY / 2 - (channelNameSizeX + s.bmpIcon.Size.Width + ChannelIconTextSpacing) / 2;

                            videoGraphics.FillRectangle(channelIconPosX, ChannelIconPosY, channelIconPosX + s.bmpIcon.Size.Width, ChannelIconPosY + s.bmpIcon.Size.Height, theme.DarkGreyLineBrush2);
                            videoGraphics.DrawBitmap(s.bmpIcon, channelIconPosX, ChannelIconPosY);
                            videoGraphics.DrawText(s.channelText, font, channelIconPosX + s.bmpIcon.Size.Width + ChannelIconTextSpacing, ChannelIconPosY + textOffsetY, theme.LightGreyFillBrush1);

                            if (s.videoChannelIndex > 0)
                            {
                                videoGraphics.DrawLine(channelPosX0, 0, channelPosX0, videoResY, theme.BlackBrush, channelLineWidth);
                            }

                            var oscMinY = (int)(ChannelIconPosY + s.bmpIcon.Size.Height + 10);
                            var oscMaxY = (int)(oscMinY + 100.0f * (resY / 1080.0f));

                            GenerateOscilloscope(s.wav, frame.wavOffset, (int)Math.Round(SampleRate * OscilloscopeWindowSize), oscLookback, oscScale, channelPosX0 + 10, oscMinY, channelPosX1 - 10, oscMaxY, oscilloscope);

                            videoGraphics.AntiAliasing = true;
                            videoGraphics.DrawLine(oscilloscope, theme.LightGreyFillBrush1);
                            videoGraphics.AntiAliasing = false;
                        }

                        // Watermark.
                        videoGraphics.DrawBitmap(bmpWatermark, videoResX - bmpWatermark.Size.Width, videoResY - bmpWatermark.Size.Height);
                        videoGraphics.EndDraw();

                        // Readback + send to ffmpeg.
                        videoGraphics.GetBitmap(videoImage);
                        stream.Write(videoImage);

                        // Dump debug images.
                        // DumpDebugImage(videoImage, videoResX, videoResY, f);
                    }
                }

                process.WaitForExit();
                process.Dispose();
                process = null;

                File.Delete(tempAudioFile);
            }
#if !DEBUG
            catch (Exception e)
            {
                Log.LogMessage(LogSeverity.Error, "Error exporting video.");
                Log.LogMessage(LogSeverity.Error, e.Message);
            }
            finally
#endif
            {
                pianoRoll.EndVideoRecording();
                foreach (var c in channelStates)
                {
                    c.bmpIcon.Dispose();
                    c.bitmap.Dispose();
                    c.graphics.Dispose();
                }
                theme.Terminate();
                bmpWatermark.Dispose();
                gradientBrush.Dispose();
                videoGraphics.Dispose();
            }

            return(true);
        }
Exemplo n.º 7
0
        public bool Save(Project originalProject, string filename, int[] songIds, bool deleteUnusedData)
        {
            var project = originalProject.DeepClone();

            project.DeleteAllSongsBut(songIds, deleteUnusedData);

            SetInvariantCulture();

            var lines = new List <string>();

            var versionString = Utils.SplitVersionNumber(PlatformUtils.ApplicationVersion, out _);
            var projectLine   = $"Project{GenerateAttribute("Version", versionString)}{GenerateAttribute("TempoMode", TempoType.Names[project.TempoMode])}";

            if (project.Name != "")
            {
                projectLine += GenerateAttribute("Name", project.Name);
            }
            if (project.Author != "")
            {
                projectLine += GenerateAttribute("Author", project.Author);
            }
            if (project.Copyright != "")
            {
                projectLine += GenerateAttribute("Copyright", project.Copyright);
            }
            if (project.PalMode)
            {
                projectLine += GenerateAttribute("PAL", true);
            }

            if (project.UsesAnyExpansionAudio)
            {
                var expansionStrings = new List <string>();

                for (int i = ExpansionType.Start; i <= ExpansionType.End; i++)
                {
                    if (project.UsesExpansionAudio(i))
                    {
                        expansionStrings.Add(ExpansionType.ShortNames[i]);
                    }
                }

                projectLine += GenerateAttribute("Expansions", string.Join(",", expansionStrings));

                if (project.UsesN163Expansion)
                {
                    projectLine += GenerateAttribute("NumN163Channels", project.ExpansionNumN163Channels);
                }
            }

            lines.Add(projectLine);

            // DPCM samples
            foreach (var sample in project.Samples)
            {
                // We don't include any DPCM sample source data or processing data. We simply write the final
                // processed data. Including giant WAV files or asking other importers to implement all the
                // processing options is unrealistic.
                if (sample.HasAnyProcessingOptions)
                {
                    if (sample.SourceDataIsWav)
                    {
                        Log.LogMessage(LogSeverity.Warning, $"Sample {sample.Name} has WAV data as source. Only the final processed DMC data will be exported.");
                    }
                    else
                    {
                        Log.LogMessage(LogSeverity.Warning, $"Sample {sample.Name} has processing option(s) enabled. Only the final processed DMC data will be exported.");
                    }
                }

                sample.PermanentlyApplyAllProcessing();

                Debug.Assert(!sample.HasAnyProcessingOptions);

                lines.Add($"\tDPCMSample{GenerateAttribute("Name", sample.Name)}{GenerateAttribute("Data", String.Join("", sample.ProcessedData.Select(x => $"{x:x2}")))}");
            }

            // DPCM mappings
            for (int i = 0; i < project.SamplesMapping.Length; i++)
            {
                var mapping = project.SamplesMapping[i];

                if (mapping != null)
                {
                    lines.Add($"\tDPCMMapping{GenerateAttribute("Note", Note.GetFriendlyName(i + Note.DPCMNoteMin))}{GenerateAttribute("Sample", mapping.Sample.Name)}{GenerateAttribute("Pitch", mapping.Pitch)}{GenerateAttribute("Loop", mapping.Loop)}");
                }
            }

            // Instruments
            foreach (var instrument in project.Instruments)
            {
                var instrumentLine = $"\tInstrument{GenerateAttribute("Name", instrument.Name)}";
                if (instrument.IsExpansionInstrument)
                {
                    instrumentLine += GenerateAttribute("Expansion", ExpansionType.ShortNames[instrument.Expansion]);

                    if (instrument.IsFdsInstrument)
                    {
                        instrumentLine += GenerateAttribute("FdsWavePreset", WavePresetType.Names[instrument.FdsWavePreset]);
                        instrumentLine += GenerateAttribute("FdsModPreset", WavePresetType.Names[instrument.FdsModPreset]);
                        if (instrument.FdsMasterVolume != 0)
                        {
                            instrumentLine += GenerateAttribute("FdsMasterVolume", instrument.FdsMasterVolume);
                        }
                        if (instrument.FdsModSpeed != 0)
                        {
                            instrumentLine += GenerateAttribute("FdsModSpeed", instrument.FdsModSpeed);
                        }
                        if (instrument.FdsModDepth != 0)
                        {
                            instrumentLine += GenerateAttribute("FdsModDepth", instrument.FdsModDepth);
                        }
                        if (instrument.FdsModDelay != 0)
                        {
                            instrumentLine += GenerateAttribute("FdsModDelay", instrument.FdsModDelay);
                        }
                    }
                    else if (instrument.IsN163Instrument)
                    {
                        instrumentLine += GenerateAttribute("N163WavePreset", WavePresetType.Names[instrument.N163WavePreset]);
                        instrumentLine += GenerateAttribute("N163WaveSize", instrument.N163WaveSize);
                        instrumentLine += GenerateAttribute("N163WavePos", instrument.N163WavePos);
                    }
                    else if (instrument.IsVrc6Instrument)
                    {
                        instrumentLine += GenerateAttribute("Vrc6SawMasterVolume", Vrc6SawMasterVolumeType.Names[instrument.Vrc6SawMasterVolume]);
                    }
                    else if (instrument.IsVrc7Instrument)
                    {
                        instrumentLine += GenerateAttribute("Vrc7Patch", instrument.Vrc7Patch);

                        if (instrument.Vrc7Patch == Vrc7InstrumentPatch.Custom)
                        {
                            for (int i = 0; i < 8; i++)
                            {
                                instrumentLine += GenerateAttribute($"Vrc7Reg{i}", instrument.Vrc7PatchRegs[i]);
                            }
                        }
                    }
                }
                lines.Add(instrumentLine);

                for (int i = 0; i < EnvelopeType.Count; i++)
                {
                    var env = instrument.Envelopes[i];
                    if (env != null && !env.IsEmpty(i))
                    {
                        var envelopeLine = $"\t\tEnvelope{GenerateAttribute("Type", EnvelopeType.ShortNames[i])}{GenerateAttribute("Length", env.Length)}";

                        if (env.Loop >= 0)
                        {
                            envelopeLine += GenerateAttribute("Loop", env.Loop);
                        }
                        if (env.Release >= 0)
                        {
                            envelopeLine += GenerateAttribute("Release", env.Release);
                        }
                        if (env.Relative)
                        {
                            envelopeLine += GenerateAttribute("Relative", env.Relative);
                        }

                        envelopeLine += GenerateAttribute("Values", String.Join(",", env.Values.Take(env.Length)));

                        lines.Add(envelopeLine);
                    }
                }
            }

            // Arpeggios
            foreach (var arpeggio in project.Arpeggios)
            {
                var env          = arpeggio.Envelope;
                var arpeggioLine = $"\tArpeggio{GenerateAttribute("Name", arpeggio.Name)}{GenerateAttribute("Length", env.Length)}";
                if (env.Loop >= 0)
                {
                    arpeggioLine += GenerateAttribute("Loop", env.Loop);
                }
                arpeggioLine += GenerateAttribute("Values", String.Join(",", env.Values.Take(env.Length)));
                lines.Add(arpeggioLine);
            }

            // Songs
            foreach (var song in project.Songs)
            {
                var songStr = $"\tSong{GenerateAttribute("Name", song.Name)}{GenerateAttribute("Length", song.Length)}{GenerateAttribute("LoopPoint", song.LoopPoint)}";

                if (song.UsesFamiTrackerTempo)
                {
                    songStr += $"{GenerateAttribute("PatternLength", song.PatternLength)}{GenerateAttribute("BeatLength", song.BeatLength)}{GenerateAttribute("FamiTrackerTempo", song.FamitrackerTempo)}{GenerateAttribute("FamiTrackerSpeed", song.FamitrackerSpeed)}";
                }
                else
                {
                    songStr += $"{GenerateAttribute("PatternLength", song.PatternLength / song.NoteLength)}{GenerateAttribute("BeatLength", song.BeatLength / song.NoteLength)}{GenerateAttribute("NoteLength", song.NoteLength)}{GenerateAttribute("Groove", string.Join("-", song.Groove))}{GenerateAttribute("GroovePaddingMode", GroovePaddingType.Names[song.GroovePaddingMode])}";
                }

                lines.Add(songStr);

                for (int i = 0; i < song.Length; i++)
                {
                    if (song.PatternHasCustomSettings(i))
                    {
                        var patternLength = song.GetPatternLength(i);

                        if (song.UsesFamiTrackerTempo)
                        {
                            lines.Add($"\t\tPatternCustomSettings{GenerateAttribute("Time", i)}{GenerateAttribute("Length", patternLength)}");
                        }
                        else
                        {
                            var noteLength        = song.GetPatternNoteLength(i);
                            var beatLength        = song.GetPatternBeatLength(i);
                            var groove            = song.GetPatternGroove(i);
                            var groovePaddingMode = song.GetPatternGroovePaddingMode(i);

                            lines.Add($"\t\tPatternCustomSettings{GenerateAttribute("Time", i)}{GenerateAttribute("Length", patternLength / noteLength)}{GenerateAttribute("NoteLength", noteLength)}{GenerateAttribute("Groove", string.Join("-", groove))}{GenerateAttribute("GroovePaddingMode", GroovePaddingType.Names[groovePaddingMode])}{GenerateAttribute("BeatLength", beatLength / noteLength)}");
                        }
                    }
                }

                foreach (var channel in song.Channels)
                {
                    lines.Add($"\t\tChannel{GenerateAttribute("Type", ChannelType.ShortNames[channel.Type])}");

                    foreach (var pattern in channel.Patterns)
                    {
                        lines.Add($"\t\t\tPattern{GenerateAttribute("Name", pattern.Name)}");

                        foreach (var kv in pattern.Notes)
                        {
                            var note = kv.Value;

                            if (!note.IsEmpty)
                            {
                                var noteLine = $"\t\t\t\tNote{GenerateAttribute("Time", kv.Key)}";

                                if (note.IsMusicalOrStop)
                                {
                                    noteLine += GenerateAttribute("Value", note.FriendlyName);

                                    if (note.IsMusical)
                                    {
                                        noteLine += GenerateAttribute("Duration", note.Duration);

                                        if (note.HasRelease)
                                        {
                                            noteLine += GenerateAttribute("Release", note.Release);
                                        }
                                        if (note.Instrument != null)
                                        {
                                            noteLine += GenerateAttribute("Instrument", note.Instrument.Name);
                                        }
                                        if (note.IsArpeggio)
                                        {
                                            noteLine += GenerateAttribute("Arpeggio", note.Arpeggio.Name);
                                        }
                                        if (note.IsSlideNote)
                                        {
                                            noteLine += GenerateAttribute("SlideTarget", Note.GetFriendlyName(note.SlideNoteTarget));
                                        }
                                    }
                                }

                                if (!note.HasAttack)
                                {
                                    noteLine += GenerateAttribute("Attack", false);
                                }
                                if (note.HasVolume)
                                {
                                    noteLine += GenerateAttribute("Volume", note.Volume);
                                }
                                if (note.HasVolumeSlide)
                                {
                                    noteLine += GenerateAttribute("VolumeSlideTarget", note.VolumeSlideTarget);
                                }
                                if (note.HasVibrato)
                                {
                                    noteLine += $"{GenerateAttribute("VibratoSpeed", note.VibratoSpeed)}{GenerateAttribute("VibratoDepth", note.VibratoDepth)}";
                                }
                                if (note.HasSpeed)
                                {
                                    noteLine += GenerateAttribute("Speed", note.Speed);
                                }
                                if (note.HasFinePitch)
                                {
                                    noteLine += GenerateAttribute("FinePitch", note.FinePitch);
                                }
                                if (note.HasFdsModSpeed)
                                {
                                    noteLine += GenerateAttribute("FdsModSpeed", note.FdsModSpeed);
                                }
                                if (note.HasFdsModDepth)
                                {
                                    noteLine += GenerateAttribute("FdsModDepth", note.FdsModDepth);
                                }
                                if (note.HasDutyCycle)
                                {
                                    noteLine += GenerateAttribute("DutyCycle", note.DutyCycle);
                                }
                                if (note.HasNoteDelay)
                                {
                                    noteLine += GenerateAttribute("NoteDelay", note.NoteDelay);
                                }
                                if (note.HasCutDelay)
                                {
                                    noteLine += GenerateAttribute("CutDelay", note.CutDelay);
                                }

                                lines.Add(noteLine);
                            }
                        }
                    }

                    for (int p = 0; p < song.Length; p++)
                    {
                        var pattern = channel.PatternInstances[p];

                        if (pattern != null)
                        {
                            lines.Add($"\t\t\tPatternInstance{GenerateAttribute("Time", p)}{GenerateAttribute("Pattern", pattern.Name)}");
                        }
                    }
                }
            }

            File.WriteAllLines(filename, lines);

            ResetCulture();
            return(true);
        }
Exemplo n.º 8
0
        const int MaxDpcmSize         = 0x2c00; // 11KB

        public unsafe bool Save(Project originalProject, string filename, int[] songIds, string name, string author, bool pal)
        {
#if !DEBUG
            try
#endif
            {
                if (songIds.Length == 0)
                {
                    return(false);
                }

                Debug.Assert(!originalProject.UsesAnyExpansionAudio || !pal);

                if (songIds.Length > MaxSongs)
                {
                    Array.Resize(ref songIds, MaxSongs);
                }

                var project = originalProject.DeepClone();
                project.DeleteAllSongsBut(songIds);
                project.SetExpansionAudioMask(ExpansionType.NoneMask);

                var headerBytes = new byte[RomHeaderLength];
                var codeBytes   = new byte[RomCodeAndTocSize + RomTileSize];

                // Load ROM header (16 bytes) + code/tiles (12KB).
                string romName = "FamiStudio.Rom.rom";
                if (project.UsesFamiTrackerTempo)
                {
                    romName += "_famitracker";
                }
                romName += pal ? "_pal" : "_ntsc";
                romName += ".nes";

                var romBinStream = typeof(RomFile).Assembly.GetManifestResourceStream(romName);
                romBinStream.Read(headerBytes, 0, RomHeaderLength);
                romBinStream.Seek(-RomCodeAndTocSize - RomTileSize, SeekOrigin.End);
                romBinStream.Read(codeBytes, 0, RomCodeAndTocSize + RomTileSize);

                Log.LogMessage(LogSeverity.Info, $"ROM code and graphics size: {codeBytes.Length} bytes.");

                // Build project info + song table of content.
                var projectInfo = BuildProjectInfo(songIds, name, author);
                var songTable   = BuildSongTableOfContent(project);

                // Gathersong data.
                var songBanks = new List <List <byte> >();

                // Export each song individually, build TOC at the same time.
                for (int i = 0; i < project.Songs.Count; i++)
                {
                    if (i == MaxSongs)
                    {
                        Log.LogMessage(LogSeverity.Warning, $"Too many songs. There is a hard limit of {MaxSongs} at the moment. Ignoring any extra songs.");
                        break;
                    }

                    var song      = project.Songs[i];
                    var songBytes = new FamitoneMusicFile(FamiToneKernel.FamiStudio, false).GetBytes(project, new int[] { song.Id }, RomSongDataStart, RomDpcmStart, pal ? MachineType.PAL : MachineType.NTSC);

                    if (songBytes.Length > MaxSongSize)
                    {
                        Log.LogMessage(LogSeverity.Warning, $"Song {song.Name} has a size of {songBytes.Length}, which is larger than the maximum allowed for ROM export ({MaxSongSize}). Truncating.");
                        Array.Resize(ref songBytes, MaxSongSize);
                    }

                    var numBanks = Utils.DivideAndRoundUp(songBytes.Length, RomBankSize);
                    Debug.Assert(numBanks <= 2);

                    var songBank = songBanks.Count;
                    var songAddr = RomSongDataStart;

                    // If single bank, look for an existing bank with some free space at the end.
                    if (numBanks == 1)
                    {
                        var foundExistingBank = false;

                        for (int j = 0; j < songBanks.Count; j++)
                        {
                            var freeSpace = RomBankSize - songBanks[j].Count;
                            if (songBytes.Length <= freeSpace)
                            {
                                songBank  = j;
                                songAddr  = RomSongDataStart + songBanks[j].Count;
                                songBytes = new FamitoneMusicFile(FamiToneKernel.FamiStudio, false).GetBytes(project, new int[] { song.Id }, songAddr, RomDpcmStart, pal ? MachineType.PAL : MachineType.NTSC);
                                Debug.Assert(songBytes.Length <= freeSpace);
                                foundExistingBank = true;
                                break;
                            }
                        }

                        // No free space found, allocation a new partial bank.
                        if (!foundExistingBank)
                        {
                            songBanks.Add(new List <byte>());
                        }

                        songBanks[songBank].AddRange(songBytes);
                    }
                    else
                    {
                        // When a song uses 2 banks, allocate a new full one and a partial one.
                        var bank0 = new List <byte>();
                        var bank1 = new List <byte>();

                        for (int j = 0; j < RomBankSize; j++)
                        {
                            bank0.Add(songBytes[j]);
                        }
                        for (int j = RomBankSize; j < songBytes.Length; j++)
                        {
                            bank1.Add(songBytes[j]);
                        }

                        songBanks.Add(bank0);
                        songBanks.Add(bank1);
                    }

                    songTable[i].bank    = (byte)songBank;
                    songTable[i].address = (ushort)songAddr;
                    songTable[i].flags   = (byte)(song.UsesDpcm ? 1 : 0);

                    Log.LogMessage(LogSeverity.Info, $"Song '{song.Name}' size: {songBytes.Length} bytes.");
                }

                //File.WriteAllBytes("D:\\debug.bin", songDataBytes.ToArray());

                // Add extra empty banks if we haven't reached the minimum.
                if (songBanks.Count < RomMinNumberBanks)
                {
                    for (int i = songBanks.Count; i < RomMinNumberBanks; i++)
                    {
                        songBanks.Add(new List <byte>());
                    }
                }
                else if ((songBanks.Count & 1) != 0)
                {
                    songBanks.Add(new List <byte>());
                }

                // Build final song bank data.
                var songBanksBytes = new byte[songBanks.Count * RomBankSize];
                for (int i = 0; i < songBanks.Count; i++)
                {
                    Array.Copy(songBanks[i].ToArray(), 0, songBanksBytes, i * RomBankSize, songBanks[i].Count);
                }

                // Patch in code (project info and song table are after the code, 0xf000).
                Marshal.Copy(new IntPtr(&projectInfo), codeBytes, RomTocOffset, sizeof(RomProjectInfo));

                for (int i = 0; i < MaxSongs; i++)
                {
                    fixed(RomSongEntry *songEntry = &songTable[i])
                    Marshal.Copy(new IntPtr(songEntry), codeBytes, RomTocOffset + sizeof(RomProjectInfo) + i * sizeof(RomSongEntry), sizeof(RomSongEntry));
                }

                // Patch header (iNES header always counts in 16KB banks, MMC3 counts in 8KB banks)
                headerBytes[RomHeaderPrgOffset] = (byte)((songBanks.Count + RomCodeDpcmNumBanks) * RomBankSize / 0x4000);

                // Build final ROM and save.
                var romBytes = new List <byte>();
                romBytes.AddRange(headerBytes);
                romBytes.AddRange(songBanksBytes);

                // Samples are at the end, right before the source engine code. MMC3 second to last and last banks respectively.
                if (project.UsesSamples)
                {
                    // Since we keep the code/engine at f000 all the time, we are limited to 12KB of samples in ROM.
                    var dpcmBytes = project.GetPackedSampleData();

                    Log.LogMessage(LogSeverity.Info, $"DPCM size: {dpcmBytes.Length} bytes.");

                    if (dpcmBytes.Length > MaxDpcmSize)
                    {
                        Log.LogMessage(LogSeverity.Warning, $"DPCM samples size ({dpcmBytes.Length}) is larger than the maximum allowed for ROM export ({MaxDpcmSize}). Truncating.");
                    }

                    // Always allocate the full 11KB of samples.
                    Array.Resize(ref dpcmBytes, MaxDpcmSize);

                    romBytes.AddRange(dpcmBytes);
                }
                else
                {
                    romBytes.AddRange(new byte[MaxDpcmSize]);
                }

                romBytes.AddRange(codeBytes);

                File.WriteAllBytes(filename, romBytes.ToArray());

                Log.LogMessage(LogSeverity.Info, $"ROM export successful, final file size {romBytes.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);
        }
Exemplo n.º 9
0
        public unsafe bool Save(Project originalProject, int songId, int loopCount, string ffmpegExecutable, string filename, int channelMask, int audioBitRate, int videoBitRate, int pianoRollZoom, bool thinNotes)
        {
            if (channelMask == 0 || loopCount < 1)
            {
                return(false);
            }

            Log.LogMessage(LogSeverity.Info, "Detecting FFmpeg...");

            if (!DetectFFmpeg(ffmpegExecutable))
            {
                return(false);
            }

            var project = originalProject.DeepClone();
            var song    = project.GetSong(songId);

            ExtendSongForLooping(song, loopCount);

            Log.LogMessage(LogSeverity.Info, "Initializing channels...");

            var frameRate        = song.Project.PalMode ? "5000773/100000" : "6009883/100000";
            var numChannels      = Utils.NumberOfSetBits(channelMask);
            var channelResXFloat = videoResX / (float)numChannels;
            var channelResX      = videoResY;
            var channelResY      = (int)channelResXFloat;

            var channelGraphics = new RenderGraphics(channelResX, channelResY);
            var videoGraphics   = new RenderGraphics(videoResX, videoResY);

            var theme        = RenderTheme.CreateResourcesForGraphics(videoGraphics);
            var bmpWatermark = videoGraphics.CreateBitmapFromResource("VideoWatermark");

            // Generate WAV data for each individual channel for the oscilloscope.
            var channelStates = new List <VideoChannelState>();

            List <short[]> channelsWavData = new List <short[]>();
            var            maxAbsSample    = 0;

            for (int i = 0, channelIndex = 0; i < song.Channels.Length; i++)
            {
                if ((channelMask & (1 << i)) == 0)
                {
                    continue;
                }

                var pattern = song.Channels[i].PatternInstances[0];
                var state   = new VideoChannelState();

                state.videoChannelIndex = channelIndex;
                state.songChannelIndex  = i;
                state.channel           = song.Channels[i];
                state.patternIndex      = 0;
                state.channelText       = state.channel.Name + (state.channel.IsExpansionChannel ? $" ({song.Project.ExpansionAudioShortName})" : "");
                state.bmp = videoGraphics.CreateBitmapFromResource(Channel.ChannelIcons[song.Channels[i].Type] + "@2x"); // HACK: Grab the 200% scaled version directly.
                state.wav = new WavPlayer(sampleRate, 1, 1 << i).GetSongSamples(song, song.Project.PalMode, -1);

                channelStates.Add(state);
                channelIndex++;

                // Find maximum absolute value to rescale the waveform.
                foreach (short s in state.wav)
                {
                    maxAbsSample = Math.Max(maxAbsSample, Math.Abs(s));
                }
            }

            // Generate the metadata for the video so we know what's happening at every frame
            var metadata = new VideoMetadataPlayer(sampleRate, 1).GetVideoMetadata(song, song.Project.PalMode, -1);

            var oscScale    = maxAbsSample != 0 ? short.MaxValue / (float)maxAbsSample : 1.0f;
            var oscLookback = (metadata[1].wavOffset - metadata[0].wavOffset) / 2;

#if FAMISTUDIO_LINUX || FAMISTUDIO_MACOS
            var dummyControl = new DummyGLControl();
            dummyControl.Move(0, 0, videoResX, videoResY);
#endif

            // Setup piano roll and images.
            var pianoRoll = new PianoRoll();
#if FAMISTUDIO_LINUX || FAMISTUDIO_MACOS
            pianoRoll.Move(0, 0, channelResX, channelResY);
#else
            pianoRoll.Width  = channelResX;
            pianoRoll.Height = channelResY;
#endif
            pianoRoll.StartVideoRecording(channelGraphics, song, pianoRollZoom, thinNotes, out var noteSizeY);

            // Build the scrolling data.
            var numVisibleNotes = (int)Math.Floor(channelResY / (float)noteSizeY);
            ComputeChannelsScroll(metadata, channelMask, numVisibleNotes);

            if (song.UsesFamiTrackerTempo)
            {
                SmoothFamiTrackerTempo(metadata);
            }

            var videoImage   = new byte[videoResY * videoResX * 4];
            var channelImage = new byte[channelResY * channelResX * 4];
            var oscilloscope = new float[channelResY, 2];

#if FAMISTUDIO_LINUX || FAMISTUDIO_MACOS
            var badAlpha = DetectBadOpenGLAlpha(dummyControl, videoGraphics, videoImage);
#endif

            // Start ffmpeg with pipe input.
            var tempFolder    = Utils.GetTemporaryDiretory();
            var tempVideoFile = Path.Combine(tempFolder, "temp.h264");
            var tempAudioFile = Path.Combine(tempFolder, "temp.wav");

            try
            {
                var process = LaunchFFmpeg(ffmpegExecutable, $"-y -f rawvideo -pix_fmt argb -s {videoResX}x{videoResY} -r {frameRate} -i - -c:v libx264 -pix_fmt yuv420p -b:v {videoBitRate}M -an \"{tempVideoFile}\"", true, false);

                // Generate each of the video frames.
                using (var stream = new BinaryWriter(process.StandardInput.BaseStream))
                {
                    for (int f = 0; f < metadata.Length; f++)
                    {
                        if (Log.ShouldAbortOperation)
                        {
                            break;
                        }

                        if ((f % 100) == 0)
                        {
                            Log.LogMessage(LogSeverity.Info, $"Rendering frame {f} / {metadata.Length}");
                        }

                        Log.ReportProgress(f / (float)(metadata.Length - 1));

                        var frame = metadata[f];

                        // Render the full screen overlay.
#if FAMISTUDIO_LINUX || FAMISTUDIO_MACOS
                        videoGraphics.BeginDraw(dummyControl, videoResY);
#else
                        videoGraphics.BeginDraw();
#endif
                        videoGraphics.Clear(Color.FromArgb(0, 0, 0, 0));

                        foreach (var s in channelStates)
                        {
                            int channelPosX0 = (int)Math.Round((s.videoChannelIndex + 0) * channelResXFloat);
                            int channelPosX1 = (int)Math.Round((s.videoChannelIndex + 1) * channelResXFloat);

                            var channelNameSizeX = videoGraphics.MeasureString(s.channelText, ThemeBase.FontBigUnscaled);
                            var channelIconPosX  = channelPosX0 + channelResY / 2 - (channelNameSizeX + s.bmp.Size.Width + channelIconTextSpacing) / 2;

                            videoGraphics.FillRectangle(channelIconPosX, channelIconPosY, channelIconPosX + s.bmp.Size.Width, channelIconPosY + s.bmp.Size.Height, theme.LightGreyFillBrush1);
                            videoGraphics.DrawBitmap(s.bmp, channelIconPosX, channelIconPosY);
                            videoGraphics.DrawText(s.channelText, ThemeBase.FontBigUnscaled, channelIconPosX + s.bmp.Size.Width + channelIconTextSpacing, channelTextPosY, theme.LightGreyFillBrush1);

                            if (s.videoChannelIndex > 0)
                            {
                                videoGraphics.DrawLine(channelPosX0, 0, channelPosX0, videoResY, theme.BlackBrush, 5);
                            }

                            GenerateOscilloscope(s.wav, frame.wavOffset, (int)Math.Round(sampleRate * oscilloscopeWindowSize), oscLookback, oscScale, channelPosX0 + 10, 60, channelPosX1 - 10, 160, oscilloscope);

                            videoGraphics.AntiAliasing = true;
                            videoGraphics.DrawLine(oscilloscope, theme.LightGreyFillBrush1);
                            videoGraphics.AntiAliasing = false;
                        }

                        videoGraphics.DrawBitmap(bmpWatermark, videoResX - bmpWatermark.Size.Width, videoResY - bmpWatermark.Size.Height);
                        videoGraphics.EndDraw();
                        videoGraphics.GetBitmap(videoImage);

                        // Render the piano rolls for each channels.
                        foreach (var s in channelStates)
                        {
                            s.volume = frame.channelVolumes[s.songChannelIndex];
                            s.note   = frame.channelNotes[s.songChannelIndex];

                            var color = Color.Transparent;

                            if (s.note.IsMusical)
                            {
                                if (s.channel.Type == Channel.Dpcm)
                                {
                                    color = Color.FromArgb(210, ThemeBase.MediumGreyFillColor1);
                                }
                                else
                                {
                                    color = Color.FromArgb(128 + s.volume * 127 / 15, s.note.Instrument != null ? s.note.Instrument.Color : ThemeBase.DarkGreyFillColor2);
                                }
                            }

#if FAMISTUDIO_LINUX || FAMISTUDIO_MACOS
                            channelGraphics.BeginDraw(pianoRoll, channelResY);
#else
                            channelGraphics.BeginDraw();
#endif
                            pianoRoll.RenderVideoFrame(channelGraphics, Channel.ChannelTypeToIndex(s.channel.Type), frame.playPattern, frame.playNote, frame.scroll[s.songChannelIndex], s.note.Value, color);
                            channelGraphics.EndDraw();

                            channelGraphics.GetBitmap(channelImage);

                            // Composite the channel image with the full screen video overlay on the CPU.
                            int channelPosX = (int)Math.Round(s.videoChannelIndex * channelResXFloat);
                            int channelPosY = 0;

                            for (int y = 0; y < channelResY; y++)
                            {
                                for (int x = 0; x < channelResX; x++)
                                {
                                    int videoIdx   = (channelPosY + x) * videoResX * 4 + (channelPosX + y) * 4;
                                    int channelIdx = (channelResY - y - 1) * channelResX * 4 + (channelResX - x - 1) * 4;

                                    byte videoA    = videoImage[videoIdx + 3];
                                    byte gradientA = (byte)(x < 255 ? 255 - x : 0); // Doing the gradient on CPU to look same on GL/D2D.

                                    byte channelR = channelImage[channelIdx + 0];
                                    byte channelG = channelImage[channelIdx + 1];
                                    byte channelB = channelImage[channelIdx + 2];

                                    if (videoA != 0 || gradientA != 0)
                                    {
#if FAMISTUDIO_LINUX || FAMISTUDIO_MACOS
                                        // Fix bad sRGB alpha.
                                        if (badAlpha)
                                        {
                                            videoA = SRGBToLinear[videoA];
                                        }
#endif
                                        videoA = Math.Max(videoA, gradientA);

                                        int videoR = videoImage[videoIdx + 0];
                                        int videoG = videoImage[videoIdx + 1];
                                        int videoB = videoImage[videoIdx + 2];

                                        // Integer alpha blend.
                                        // Note that alpha is pre-multiplied, so we if we multiply again, image will look aliased.
                                        channelR = (byte)((channelR * (255 - videoA) + videoR * 255 /*videoA*/) >> 8);
                                        channelG = (byte)((channelG * (255 - videoA) + videoG * 255 /*videoA*/) >> 8);
                                        channelB = (byte)((channelB * (255 - videoA) + videoB * 255 /*videoA*/) >> 8);
                                    }

                                    // We byteswap here to match what ffmpeg expect.
                                    videoImage[videoIdx + 3] = channelR;
                                    videoImage[videoIdx + 2] = channelG;
                                    videoImage[videoIdx + 1] = channelB;
                                    videoImage[videoIdx + 0] = 255;

                                    // To export images to debug.
                                    //videoImage[videoIdx + 0] = channelR;
                                    //videoImage[videoIdx + 1] = channelG;
                                    //videoImage[videoIdx + 2] = channelB;
                                    //videoImage[videoIdx + 3] = 255;
                                }
                            }

                            var prevChannelEndPosX = (int)Math.Round((s.videoChannelIndex - 1) * channelResXFloat) + channelResY;

                            // HACK: Since we round the channels positions, we can end up with columns of pixels that arent byteswapped.
                            if (s.videoChannelIndex > 0 && channelPosX != prevChannelEndPosX)
                            {
                                for (int y = 0; y < videoResY; y++)
                                {
                                    int videoIdx = y * videoResX * 4 + (channelPosX - 1) * 4;

                                    byte videoR = videoImage[videoIdx + 0];
                                    byte videoG = videoImage[videoIdx + 1];
                                    byte videoB = videoImage[videoIdx + 2];

                                    videoImage[videoIdx + 3] = videoR;
                                    videoImage[videoIdx + 2] = videoG;
                                    videoImage[videoIdx + 1] = videoB;
                                    videoImage[videoIdx + 0] = 255;
                                }
                            }
                        }

                        stream.Write(videoImage);

                        // Dump debug images.
#if FAMISTUDIO_LINUX || FAMISTUDIO_MACOS
                        //var pb = new Gdk.Pixbuf(channelImage, true, 8, channelResX, channelResY, channelResX * 4);
                        //pb.Save($"/home/mat/Downloads/channel.png", "png");
                        //var pb = new Gdk.Pixbuf(videoImage, true, 8, videoResX, videoResY, videoResX * 4);
                        //pb.Save($"/home/mat/Downloads/frame_{f:D4}.png", "png");
#else
                        //fixed (byte* vp = &videoImage[0])
                        //{
                        //    var b = new System.Drawing.Bitmap(videoResX, videoResY, videoResX * 4, System.Drawing.Imaging.PixelFormat.Format32bppArgb, new IntPtr(vp));
                        //    b.Save($"d:\\dump\\pr\\frame_{f:D4}.png");
                        //}
#endif
                    }
                }

                process.WaitForExit();
                process.Dispose();
                process = null;

                Log.LogMessage(LogSeverity.Info, "Exporting audio...");

                // Save audio to temporary file.
                WaveFile.Save(song, tempAudioFile, sampleRate, 1, -1, channelMask);

                Log.LogMessage(LogSeverity.Info, "Mixing audio and video...");

                // Run ffmpeg again to combine audio + video.
                process = LaunchFFmpeg(ffmpegExecutable, $"-y -i \"{tempVideoFile}\" -i \"{tempAudioFile}\" -c:v copy -c:a aac -b:a {audioBitRate}k \"{filename}\"", false, false);
                process.WaitForExit();
                process.Dispose();
                process = null;

                File.Delete(tempAudioFile);
                File.Delete(tempVideoFile);
            }
            catch (Exception e)
            {
                Log.LogMessage(LogSeverity.Error, "Error exporting video.");
                Log.LogMessage(LogSeverity.Error, e.Message);
            }
            finally
            {
                pianoRoll.EndVideoRecording();
                foreach (var c in channelStates)
                {
                    c.bmp.Dispose();
                }
                theme.Terminate();
                bmpWatermark.Dispose();
                channelGraphics.Dispose();
                videoGraphics.Dispose();
            }

            return(true);
        }
Exemplo n.º 10
0
        public bool Save(Project originalProject, string filename, int[] songIds)
        {
            var project = originalProject.DeepClone();

            project.RemoveAllSongsBut(songIds);

            var lines = new List <string>();

            var versionString = Application.ProductVersion.Substring(0, Application.ProductVersion.LastIndexOf('.'));
            var projectLine   = $"Project Version=\"{versionString}\" TempoMode=\"{Project.TempoModeNames[project.TempoMode]}\"";

            if (project.Name != "")
            {
                projectLine += $" Name=\"{project.Name}\"";
            }
            if (project.Author != "")
            {
                projectLine += $" Author=\"{project.Author}\"";
            }
            if (project.Copyright != "")
            {
                projectLine += $" Copyright=\"{project.Copyright}\"";
            }
            if (project.UsesExpansionAudio)
            {
                projectLine += $" Expansion=\"{Project.ExpansionShortNames[project.ExpansionAudio]}\"";
            }
            if (project.PalMode)
            {
                projectLine += $" PAL=\"{true}\"";
            }

            lines.Add(projectLine);

            // DPCM samples
            foreach (var sample in project.Samples)
            {
                lines.Add($"\tDPCMSample Name=\"{sample.Name}\" Data=\"{String.Join("", sample.Data.Select(x => $"{x:x2}"))}\"");
            }

            // DPCM mappings
            for (int i = 0; i < project.SamplesMapping.Length; i++)
            {
                var mapping = project.SamplesMapping[i];

                if (mapping != null && mapping.Sample != null)
                {
                    lines.Add($"\tDPCMMapping Note=\"{Note.GetFriendlyName(i + Note.DPCMNoteMin)}\" Sample=\"{mapping.Sample.Name}\" Pitch=\"{mapping.Pitch}\" Loop=\"{mapping.Loop}\"");
                }
            }

            // Instruments
            foreach (var instrument in project.Instruments)
            {
                var instrumentLine = $"\tInstrument Name=\"{instrument.Name}\"";
                if (instrument.IsExpansionInstrument)
                {
                    instrumentLine += $" Expansion=\"{Project.ExpansionShortNames[project.ExpansionAudio]}\"";

                    if (instrument.ExpansionType == Project.ExpansionFds)
                    {
                        instrumentLine += $" FdsWavePreset=\"{Envelope.PresetNames[instrument.FdsWavePreset]}\"";
                        instrumentLine += $" FdsModPreset=\"{Envelope.PresetNames[instrument.FdsModPreset]}\"";
                        if (instrument.FdsMasterVolume != 0)
                        {
                            instrumentLine += $" FdsMasterVolume=\"{instrument.FdsMasterVolume}\"";
                        }
                        if (instrument.FdsModSpeed != 0)
                        {
                            instrumentLine += $" FdsModSpeed=\"{instrument.FdsModSpeed}\"";
                        }
                        if (instrument.FdsModDepth != 0)
                        {
                            instrumentLine += $" FdsModDepth=\"{instrument.FdsModDepth}\"";
                        }
                        if (instrument.FdsModDelay != 0)
                        {
                            instrumentLine += $" FdsModDelay=\"{instrument.FdsModDelay}\"";
                        }
                    }
                    else if (instrument.ExpansionType == Project.ExpansionN163)
                    {
                        instrumentLine += $" N163WavePreset=\"{Envelope.PresetNames[instrument.N163WavePreset]}\"";
                        instrumentLine += $" N163WaveSize=\"{instrument.N163WaveSize}\"";
                        instrumentLine += $" N163WavePos=\"{instrument.N163WavePos}\"";
                    }
                    else if (instrument.ExpansionType == Project.ExpansionVrc7)
                    {
                        instrumentLine += $" Vrc7Patch=\"{instrument.Vrc7Patch}\"";

                        if (instrument.Vrc7Patch == 0)
                        {
                            for (int i = 0; i < 8; i++)
                            {
                                instrumentLine += $" Vrc7Reg{i}=\"{instrument.Vrc7PatchRegs[i]}\"";
                            }
                        }
                    }
                }
                lines.Add(instrumentLine);

                for (int i = 0; i < Envelope.Count; i++)
                {
                    var env = instrument.Envelopes[i];
                    if (env != null && !env.IsEmpty)
                    {
                        var envelopeLine = $"\t\tEnvelope Type=\"{Envelope.EnvelopeShortNames[i]}\" Length=\"{env.Length}\"";

                        if (env.Loop >= 0)
                        {
                            envelopeLine += $" Loop=\"{env.Loop}\"";
                        }
                        if (env.Release >= 0)
                        {
                            envelopeLine += $" Release=\"{env.Release}\"";
                        }
                        if (env.Relative)
                        {
                            envelopeLine += $" Relative=\"{env.Relative}\"";
                        }

                        envelopeLine += $" Values=\"{String.Join(",", env.Values.Take(env.Length))}\"";

                        lines.Add(envelopeLine);
                    }
                }
            }

            // Songs
            foreach (var song in project.Songs)
            {
                var songStr = $"\tSong Name=\"{song.Name}\" Length=\"{song.Length}\" LoopPoint=\"{song.LoopPoint}\"";

                if (song.UsesFamiTrackerTempo)
                {
                    songStr += $" PatternLength=\"{song.PatternLength}\" BarLength=\"{song.BarLength}\" FamiTrackerTempo=\"{song.FamitrackerTempo}\" FamiTrackerSpeed=\"{song.FamitrackerSpeed}\"";
                }
                else
                {
                    songStr += $" PatternLength=\"{song.PatternLength / song.NoteLength}\" BarLength=\"{song.BarLength / song.NoteLength}\" NoteLength=\"{song.NoteLength}\"";
                }

                lines.Add(songStr);

                for (int i = 0; i < song.Length; i++)
                {
                    if (song.PatternHasCustomSettings(i))
                    {
                        var patternLength = song.GetPatternLength(i);

                        if (song.UsesFamiTrackerTempo)
                        {
                            lines.Add($"\t\tPatternCustomSettings Time=\"{i}\" Length=\"{patternLength}\"");
                        }
                        else
                        {
                            var noteLength = song.GetPatternNoteLength(i);
                            var barLength  = song.GetPatternBarLength(i);

                            lines.Add($"\t\tPatternCustomSettings Time=\"{i}\" Length=\"{patternLength / noteLength}\" NoteLength=\"{noteLength}\" BarLength=\"{barLength / noteLength}\"");
                        }
                    }
                }

                foreach (var channel in song.Channels)
                {
                    lines.Add($"\t\tChannel Type=\"{Channel.ChannelExportNames[channel.Type]}\"");

                    foreach (var pattern in channel.Patterns)
                    {
                        lines.Add($"\t\t\tPattern Name=\"{pattern.Name}\"");

                        foreach (var kv in pattern.Notes)
                        {
                            var note = kv.Value;

                            if (!note.IsEmpty)
                            {
                                var noteLine = $"\t\t\t\tNote Time=\"{kv.Key}\"";

                                if (note.IsValid)
                                {
                                    noteLine += $" Value=\"{note.FriendlyName}\"";
                                    if (note.Instrument != null)
                                    {
                                        noteLine += $" Instrument=\"{note.Instrument.Name}\"";
                                    }
                                }
                                if (!note.HasAttack)
                                {
                                    noteLine += $" Attack=\"{false.ToString()}\"";
                                }
                                if (note.HasVolume)
                                {
                                    noteLine += $" Volume=\"{note.Volume}\"";
                                }
                                if (note.HasVibrato)
                                {
                                    noteLine += $" VibratoSpeed=\"{note.VibratoSpeed}\" VibratoDepth=\"{note.VibratoDepth}\"";
                                }
                                if (note.HasSpeed)
                                {
                                    noteLine += $" Speed=\"{note.Speed}\"";
                                }
                                if (note.HasFinePitch)
                                {
                                    noteLine += $" FinePitch=\"{note.FinePitch}\"";
                                }
                                if (note.HasFdsModSpeed)
                                {
                                    noteLine += $" FdsModSpeed=\"{note.FdsModSpeed}\"";
                                }
                                if (note.HasFdsModDepth)
                                {
                                    noteLine += $" FdsModDepth=\"{note.FdsModDepth}\"";
                                }
                                if (note.IsMusical && note.IsSlideNote)
                                {
                                    noteLine += $" SlideTarget=\"{Note.GetFriendlyName(note.SlideNoteTarget)}\"";
                                }

                                lines.Add(noteLine);
                            }
                        }
                    }

                    for (int p = 0; p < song.Length; p++)
                    {
                        var pattern = channel.PatternInstances[p];

                        if (pattern != null)
                        {
                            lines.Add($"\t\t\tPatternInstance Time=\"{p}\" Pattern=\"{pattern.Name}\"");
                        }
                    }
                }
            }

            File.WriteAllLines(filename, lines);

            return(true);
        }
Exemplo n.º 11
0
        public unsafe static bool Save(Project originalProject, string filename, int[] songIds, string name, string author, bool pal)
        {
            try
            {
                if (songIds.Length == 0)
                {
                    return(false);
                }

                Debug.Assert(!originalProject.UsesExpansionAudio || !pal);

                if (songIds.Length > MaxSongs)
                {
                    Array.Resize(ref songIds, MaxSongs);
                }

                var project = originalProject.DeepClone();
                project.RemoveAllSongsBut(songIds);
                project.SetExpansionAudio(Project.ExpansionNone);

                var headerBytes = new byte[RomHeaderLength];
                var codeBytes   = new byte[RomCodeSize + RomTileSize];

                // Load ROM header (16 bytes) + code/tiles (12KB).
                Stream romBinStream = null;

                string romName = "FamiStudio.Rom.rom";
                romName += pal ? "_pal" : "_ntsc";
                if (project.UsesFamiStudioTempo)
                {
                    romName += "_tempo";
                }
                romName += ".nes";

                romBinStream = typeof(RomFile).Assembly.GetManifestResourceStream(romName);
                romBinStream.Read(headerBytes, 0, RomHeaderLength);
                romBinStream.Seek(-RomCodeSize - RomTileSize, SeekOrigin.End);
                romBinStream.Read(codeBytes, 0, RomCodeSize + RomTileSize);

                // Build project info + song table of content.
                var projectInfo = new RomProjectInfo();
                projectInfo.maxSong = (byte)(songIds.Length - 1);
                Marshal.Copy(EncodeAndCenterString(name), 0, new IntPtr(projectInfo.name), 28);
                Marshal.Copy(EncodeAndCenterString(author), 0, new IntPtr(projectInfo.author), 28);

                var songTable = new RomSongEntry[MaxSongs];
                for (int i = 0; i < project.Songs.Count; i++)
                {
                    fixed(RomSongEntry *songEntry = &songTable[i])
                    {
                        songEntry->page    = 0;
                        songEntry->address = 0x8000;
                        Marshal.Copy(EncodeAndCenterString(project.Songs[i].Name), 0, new IntPtr(songEntry->name), 28);
                    }
                }

                // Gather DPCM + song data.
                var songDataBytes = new List <byte>();
                var dpcmBaseAddr  = RomDpcmOffset;

                // We will put samples right at the beginning.
                if (project.UsesSamples)
                {
                    // Since we keep the code/engine at f000 all the time, we are limited to 12KB of samples in ROM.
                    var totalSampleSize = project.GetTotalSampleSize();
                    var dpcmPageCount   = Math.Min(MaxDpcmPages, (totalSampleSize + (RomPageSize - 1)) / RomPageSize);

                    // Otherwise we will allocate at least a full page for the samples and use the following mapping:
                    //    0KB -  4KB samples: starts at 0xe000
                    //    4KB -  8KB samples: starts at 0xd000
                    //    8KB - 12KB samples: starts at 0xc000
                    dpcmBaseAddr += (MaxDpcmPages - dpcmPageCount) * RomPageSize;

                    var dpcmBytes = project.GetPackedSampleData();
                    if (dpcmBytes.Length > (MaxDpcmPages * RomPageSize))
                    {
                        Array.Resize(ref dpcmBytes, MaxDpcmPages * RomPageSize);
                    }

                    songDataBytes.AddRange(dpcmBytes);

                    projectInfo.dpcmPageCount = (byte)dpcmPageCount;
                    projectInfo.dpcmPageStart = (byte)0;
                }

                // Export each song individually, build TOC at the same time.
                for (int i = 0; i < project.Songs.Count; i++)
                {
                    var song      = project.Songs[i];
                    int page      = songDataBytes.Count / RomPageSize;
                    int addr      = RomMemoryStart + (songDataBytes.Count & (RomPageSize - 1));
                    var songBytes = new FamitoneMusicFile(FamitoneMusicFile.FamiToneKernel.FamiStudio).GetBytes(project, new int[] { song.Id }, addr, dpcmBaseAddr, MachineType.NTSC);

                    songTable[i].page    = (byte)(page);
                    songTable[i].address = (ushort)(addr);
                    songTable[i].flags   = (byte)(song.UsesDpcm ? 1 : 0);

                    songDataBytes.AddRange(songBytes);
                }

                //File.WriteAllBytes("D:\\debug.bin", songDataBytes.ToArray());

                int numPrgBanks = RomMinSize / RomPrgBankSize;

                if (songDataBytes.Count > (RomMinSize - RomCodeSize))
                {
                    numPrgBanks = (songDataBytes.Count + (RomPrgBankSize - 1)) / RomPrgBankSize;
                }

                int padding = (numPrgBanks * RomPrgBankSize) - RomCodeSize - songDataBytes.Count;
                songDataBytes.AddRange(new byte[padding]);

                // Patch in code (project info and song table are at the beginning, 0xf000).
                Marshal.Copy(new IntPtr(&projectInfo), codeBytes, 0, sizeof(RomProjectInfo));

                for (int i = 0; i < MaxSongs; i++)
                {
                    fixed(RomSongEntry *songEntry = &songTable[i])
                    Marshal.Copy(new IntPtr(songEntry), codeBytes, sizeof(RomProjectInfo) + i * sizeof(RomSongEntry), sizeof(RomSongEntry));
                }

                // Patch header.
                headerBytes[RomHeaderPrgOffset] = (byte)numPrgBanks;

                // Build final ROM and save.
                var romBytes = new List <byte>();
                romBytes.AddRange(headerBytes);
                romBytes.AddRange(songDataBytes);
                romBytes.AddRange(codeBytes);

                File.WriteAllBytes(filename, romBytes.ToArray());
            }
            catch
            {
                return(false);
            }

            return(true);
        }
Exemplo n.º 12
0
        public bool Save(Project originalProject, string filename, int[] songIds)
        {
            var project = originalProject.DeepClone();

            project.RemoveAllSongsBut(songIds);

            if (project.UsesFamiStudioTempo)
            {
                project.ConvertToFamiTrackerTempo(false);
            }

            ConvertPitchEnvelopes(project);
            var envelopes = MergeIdenticalEnvelopes(project);

            var lines = new List <string>();

            lines.Add("# FamiTracker text export 0.4.2");
            lines.Add("");

            lines.Add("# Song information");
            lines.Add("TITLE           \"" + project.Name + "\"");
            lines.Add("AUTHOR          \"" + project.Author + "\"");
            lines.Add("COPYRIGHT       \"" + project.Copyright + "\"");
            lines.Add("");

            lines.Add("# Song comment");
            lines.Add("COMMENT         \"\"");

            lines.Add("# Global settings");
            lines.Add("MACHINE         0");
            lines.Add("FRAMERATE       0");
            lines.Add("EXPANSION       " + (project.ExpansionAudio != Project.ExpansionNone ? (1 << (project.ExpansionAudio - 1)) : 0));
            lines.Add("VIBRATO         1");
            lines.Add("SPLIT           32");
            lines.Add("");

            var realNumExpansionChannels = project.ExpansionNumChannels;

            if (project.ExpansionAudio == Project.ExpansionN163)
            {
                lines.Add("# Namco 163 global settings");
                lines.Add($"N163CHANNELS    {project.ExpansionNumChannels}");
                lines.Add("");

                // The text format always export all 8 channels, even if there are less.
                project.SetExpansionAudio(Project.ExpansionN163, 8);
            }

            lines.Add("# Macros");
            for (int i = 0; i < Envelope.RegularCount; i++)
            {
                var envArray = envelopes[Project.ExpansionNone, i];
                for (int j = 0; j < envArray.Length; j++)
                {
                    var env = envArray[j];
                    lines.Add($"MACRO{ReverseEnvelopeTypeLookup[i],8} {j,4} {env.Loop,4} {(env.Release >= 0 ? env.Release - 1 : -1),4}   0 : {string.Join(" ", env.Values.Take(env.Length))}");
                }
            }

            if (project.ExpansionAudio == Project.ExpansionVrc6 ||
                project.ExpansionAudio == Project.ExpansionN163)
            {
                var suffix = project.ExpansionAudio == Project.ExpansionVrc6 ? "VRC6" : "N163";

                for (int i = 0; i < Envelope.RegularCount; i++)
                {
                    var envArray = envelopes[1, i];
                    for (int j = 0; j < envArray.Length; j++)
                    {
                        var env = envArray[j];
                        lines.Add($"MACRO{suffix}{i,8} {j,4} {env.Loop,4} {(env.Release >= 0 ? env.Release - 1 : -1),4}   0 : {string.Join(" ", env.Values.Take(env.Length))}");
                    }
                }
            }

            lines.Add("");

            if (project.UsesSamples)
            {
                lines.Add("# DPCM samples");
                for (int i = 0; i < project.Samples.Count; i++)
                {
                    var sample = project.Samples[i];
                    lines.Add($"DPCMDEF{i,4}{sample.Data.Length,6} \"{sample.Name}\"");
                    lines.Add($"DPCM : {String.Join(" ", sample.Data.Select(x => $"{x:X2}"))}");
                }
                lines.Add("");
            }

            lines.Add("# Instruments");
            for (int i = 0; i < project.Instruments.Count; i++)
            {
                var instrument = project.Instruments[i];

                var volEnv = instrument.Envelopes[Envelope.Volume];
                var arpEnv = instrument.Envelopes[Envelope.Arpeggio];
                var pitEnv = instrument.Envelopes[Envelope.Pitch];
                var dutEnv = instrument.Envelopes[Envelope.DutyCycle];

                var expIdx    = instrument.IsExpansionInstrument ? 1 : 0;
                int volEnvIdx = volEnv != null && volEnv.Length > 0 ? Array.IndexOf(envelopes[expIdx, Envelope.Volume], instrument.Envelopes[Envelope.Volume])    : -1;
                int arpEnvIdx = arpEnv != null && arpEnv.Length > 0 ? Array.IndexOf(envelopes[expIdx, Envelope.Arpeggio], instrument.Envelopes[Envelope.Arpeggio])  : -1;
                int pitEnvIdx = pitEnv != null && pitEnv.Length > 0 ? Array.IndexOf(envelopes[expIdx, Envelope.Pitch], instrument.Envelopes[Envelope.Pitch])     : -1;
                int dutEnvIdx = dutEnv != null && dutEnv.Length > 0 ? Array.IndexOf(envelopes[expIdx, Envelope.DutyCycle], instrument.Envelopes[Envelope.DutyCycle]) : -1;

                if (instrument.ExpansionType == Project.ExpansionNone)
                {
                    lines.Add($"INST2A03{i,4}{volEnvIdx,6}{arpEnvIdx,4}{pitEnvIdx,4}{-1,4}{dutEnvIdx,4} \"{instrument.Name}\"");
                }
                else if (instrument.ExpansionType == Project.ExpansionVrc6)
                {
                    lines.Add($"INSTVRC6{i,4}{volEnvIdx,6}{arpEnvIdx,4}{pitEnvIdx,4}{-1,4}{dutEnvIdx,4} \"{instrument.Name}\"");
                }
                else if (instrument.ExpansionType == Project.ExpansionVrc7)
                {
                    lines.Add($"INSTVRC7{i,4}{instrument.Vrc7Patch,4} {String.Join(" ", instrument.Vrc7PatchRegs.Select(x => $"{x:X2}"))} \"{instrument.Name}\"");
                }
                else if (instrument.ExpansionType == Project.ExpansionN163)
                {
                    lines.Add($"INSTN163{i,4}{volEnvIdx,6}{arpEnvIdx,4}{pitEnvIdx,4}{-1,4}{dutEnvIdx,4}{instrument.N163WaveSize,4}{instrument.N163WavePos,4}{1,4} \"{instrument.Name}\"");

                    var wavEnv = instrument.Envelopes[Envelope.N163Waveform];
                    lines.Add($"N163WAVE{i,4}{0,6} : {string.Join(" ", wavEnv.Values.Take(wavEnv.Length))}");
                }
                else if (instrument.ExpansionType == Project.ExpansionFds)
                {
                    lines.Add($"INSTFDS{i,5}{1,6}{instrument.FdsModSpeed,4}{instrument.FdsModDepth,4}{instrument.FdsModDelay,4} \"{instrument.Name}\"");

                    var wavEnv = instrument.Envelopes[Envelope.FdsWaveform];
                    lines.Add($"FDSWAVE{i,5} : {string.Join(" ", wavEnv.Values.Take(wavEnv.Length))}");
                    var modEnv = instrument.Envelopes[Envelope.FdsModulation].BuildFdsModulationTable();
                    lines.Add($"FDSMOD{i,6} : {string.Join(" ", modEnv.Take(modEnv.Length))}");

                    for (int j = 0; j <= Envelope.Pitch; j++)
                    {
                        var env = instrument.Envelopes[j];
                        if (!env.IsEmpty)
                        {
                            lines.Add($"FDSMACRO{i,4} {j,5} {env.Loop,4} {(env.Release >= 0 ? env.Release - 1 : -1),4}   0 : {string.Join(" ", env.Values.Take(env.Length))}");
                        }
                    }
                }
            }

            if (project.UsesSamples)
            {
                lines.Add($"INST2A03{project.Instruments.Count,4}{-1,6}{-1,4}{-1,4}{-1,4}{-1,4} \"DPCM\"");

                for (int i = 0; i < project.SamplesMapping.Length; i++)
                {
                    var mapping = project.SamplesMapping[i];

                    if (mapping != null && mapping.Sample != null)
                    {
                        int note     = i + Note.DPCMNoteMin;
                        var octave   = (note - 1) / 12;
                        var semitone = (note - 1) % 12;
                        var idx      = project.Samples.IndexOf(mapping.Sample);
                        var loop     = mapping.Loop ? 1 : 0;

                        lines.Add($"KEYDPCM{project.Instruments.Count,4}{octave,4}{semitone,4}{idx,6}{mapping.Pitch,4}{loop,4}{0,6}{-1,4}");
                    }
                }
            }
            lines.Add("");

            lines.Add("# Tracks");
            for (int i = 0; i < project.Songs.Count; i++)
            {
                var song = project.Songs[i];

                TruncateLongPatterns(song);
                CreateMissingPatterns(song);
                song.CleanupUnusedPatterns();
                song.DuplicateInstancesWithDifferentLengths();

                lines.Add($"TRACK{song.PatternLength,4}{song.FamitrackerSpeed,4}{song.FamitrackerTempo,4} \"{song.Name}\"");
                lines.Add($"COLUMNS : {string.Join(" ", Enumerable.Repeat(3, song.Channels.Length))}");
                lines.Add("");

                for (int j = 0; j < song.Length; j++)
                {
                    var line = $"ORDER {j:X2} :";

                    for (int k = 0; k < song.Channels.Length; k++)
                    {
                        line += $" {song.Channels[k].Patterns.IndexOf(song.Channels[k].PatternInstances[j]):X2}";
                    }

                    lines.Add(line);
                }
                lines.Add("");

                int maxPatternCount = -1;
                foreach (var channel in song.Channels)
                {
                    maxPatternCount = Math.Max(maxPatternCount, channel.Patterns.Count);
                }

                var patternRows = new Dictionary <Pattern, List <string> >();
                for (int c = 0; c < song.Channels.Length; c++)
                {
                    var channel         = song.Channels[c];
                    var prevNoteValue   = Note.NoteInvalid;
                    var prevInstrument  = (Instrument)null;
                    var prevSlideEffect = Effect_None;

                    for (int p = 0; p < song.Length; p++)
                    {
                        var pattern    = channel.PatternInstances[p];
                        var patternLen = song.GetPatternLength(p);

                        if (patternRows.ContainsKey(pattern))
                        {
                            continue;
                        }

                        var patternLines = new List <string>();

                        for (var it = pattern.GetNoteIterator(0, song.PatternLength); !it.Done; it.Next())
                        {
                            var time = it.CurrentTime;
                            var note = it.CurrentNote;

                            // Keeps the code a lot simpler.
                            if (note == null)
                            {
                                note = Note.EmptyNote;
                            }

                            var line = " : ... .. . ... ... ...";

                            var noteString       = GetFamiTrackerNoteName(c, note);
                            var volumeString     = note.HasVolume ? note.Volume.ToString("X") : ".";
                            var instrumentString = note.IsValid && !note.IsStop ? (note.Instrument == null ? project.Instruments.Count : project.Instruments.IndexOf(note.Instrument)).ToString("X2") : "..";
                            var effectString     = "";
                            var noAttack         = !note.HasAttack && prevNoteValue == note.Value && (prevSlideEffect == Effect_None || prevSlideEffect == Effect_SlideUp || prevSlideEffect == Effect_Portamento);

                            if (note.IsSlideNote && note.IsMusical)
                            {
                                var noteTable = NesApu.GetNoteTableForChannelType(channel.Type, false, realNumExpansionChannels);

                                var noteValue   = note.Value;
                                var slideTarget = note.SlideNoteTarget;

                                // FamiTracker only has 12-pitches and doesnt change the octave when doing
                                // slides. This helps make the slides more compatible, but its not great.
                                if (channel.IsVrc7FmChannel)
                                {
                                    while (noteValue >= 12 && slideTarget >= 12)
                                    {
                                        noteValue   -= 12;
                                        slideTarget -= 12;
                                    }
                                }

                                // TODO: We use the initial FamiTracker speed here, this is wrong, it might have changed. Also we assume NTSC here.
                                var stepSizeFloat = channel.ComputeRawSlideNoteParams(noteValue, slideTarget, p, time, song.FamitrackerSpeed, Song.NativeTempoNTSC, noteTable);

                                if (channel.IsN163WaveChannel)
                                {
                                    stepSizeFloat /= 4.0f;
                                }

                                // Undo any kind of shifting we had done. This will kill the 1-bit of fraction we have on most channel.
                                var absNoteDelta = Math.Abs(note.Value - note.SlideNoteTarget);

                                // See if we can use Qxy/Rxy (slide up/down y semitones, at speed x), this is preferable.
                                if (absNoteDelta < 16)
                                {
                                    if (prevSlideEffect == Effect_PortaUp ||
                                        prevSlideEffect == Effect_PortaDown ||
                                        prevSlideEffect == Effect_Portamento)
                                    {
                                        effectString += $" {EffectToTextLookup[prevSlideEffect]}00";
                                    }

                                    // FamiTracker use 2x + 1, find the number that is just above our speed.
                                    var speed = 0;
                                    for (int x = 14; x >= 0; x--)
                                    {
                                        if ((2 * x + 1) < Math.Abs(stepSizeFloat))
                                        {
                                            speed = x + 1;
                                            break;
                                        }
                                    }

                                    if (note.SlideNoteTarget > note.Value)
                                    {
                                        effectString += $" Q{speed:X1}{absNoteDelta:X1}";
                                    }
                                    else
                                    {
                                        effectString += $" R{speed:X1}{absNoteDelta:X1}";
                                    }

                                    prevSlideEffect = Effect_SlideUp;
                                }
                                else
                                {
                                    // We have one bit of fraction. FramiTracker does not.
                                    var ceilStepSize = Utils.SignedCeil(stepSizeFloat);

                                    // If the previous note matched too, we can use 3xx (auto-portamento).
                                    // Avoid using portamento on instrument with relative pitch envelopes, their previous pitch isnt reliable.
                                    if (prevNoteValue == note.Value && (prevInstrument == null || prevInstrument.Envelopes[Envelope.Pitch].IsEmpty || !prevInstrument.Envelopes[Envelope.Pitch].Relative))
                                    {
                                        if (prevSlideEffect == Effect_PortaUp ||
                                            prevSlideEffect == Effect_PortaDown)
                                        {
                                            effectString += $" 100";
                                        }

                                        noteString      = GetFamiTrackerNoteName(c, new Note(note.SlideNoteTarget));
                                        effectString   += $" 3{Math.Min(0xff, Math.Abs(ceilStepSize)):X2}";
                                        prevSlideEffect = Effect_Portamento;
                                        noAttack        = false; // Need to force attack when starting auto-portamento unfortunately.
                                    }
                                    else
                                    {
                                        // Inverted channels.
                                        if (channel.IsFdsWaveChannel || channel.IsN163WaveChannel)
                                        {
                                            stepSizeFloat = -stepSizeFloat;
                                        }

                                        var absFloorStepSize = Math.Abs(Utils.SignedFloor(stepSizeFloat));

                                        if (prevSlideEffect == Effect_Portamento)
                                        {
                                            effectString += $" 300";
                                        }

                                        if (note.SlideNoteTarget > note.Value)
                                        {
                                            effectString   += $" 1{Math.Min(0xff, absFloorStepSize):X2}";
                                            prevSlideEffect = Effect_PortaUp;
                                        }
                                        else if (note.SlideNoteTarget < note.Value)
                                        {
                                            effectString   += $" 2{Math.Min(0xff, absFloorStepSize):X2}";
                                            prevSlideEffect = Effect_PortaDown;
                                        }
                                    }
                                }
                            }
                            else if ((note.IsMusical || note.IsStop) && prevSlideEffect != Effect_None)
                            {
                                if (prevSlideEffect == Effect_PortaUp ||
                                    prevSlideEffect == Effect_PortaDown ||
                                    prevSlideEffect == Effect_Portamento)
                                {
                                    effectString += $" {EffectToTextLookup[prevSlideEffect]}00";
                                }

                                prevSlideEffect = Effect_None;
                            }

                            if (time == patternLen - 1)
                            {
                                if (p == song.Length - 1 && song.LoopPoint >= 0)
                                {
                                    effectString += $" B{song.LoopPoint:X2}";
                                }
                                else if (patternLen != song.PatternLength)
                                {
                                    effectString += $" D00";
                                }
                            }

                            if (note.HasSpeed)
                            {
                                effectString += $" F{note.Speed:X2}";
                            }
                            if (note.HasVibrato)
                            {
                                effectString += $" 4{VibratoSpeedExportLookup[note.VibratoSpeed]:X1}{note.VibratoDepth:X1}";
                            }
                            if (note.HasFinePitch)
                            {
                                effectString += $" P{(byte)(-note.FinePitch + 0x80):X2}";
                            }
                            if (note.HasFdsModDepth)
                            {
                                effectString += $" H{note.FdsModDepth:X2}";
                            }
                            if (note.HasFdsModSpeed)
                            {
                                effectString += $" I{(note.FdsModSpeed >> 8) & 0xff:X2}";
                                effectString += $" J{(note.FdsModSpeed >> 0) & 0xff:X2}";
                            }

                            while (effectString.Length < 12)
                            {
                                effectString += " ...";
                            }

                            if (noAttack)
                            {
                                noteString       = "...";
                                instrumentString = "..";
                            }

                            line = $" : {noteString} {instrumentString} {volumeString}{effectString}";

                            if (note.IsMusical || note.IsStop)
                            {
                                prevNoteValue = note.IsSlideNote ? note.SlideNoteTarget : note.Value;
                                if (note.IsMusical)
                                {
                                    prevInstrument = note.Instrument;
                                }
                            }

                            patternLines.Add(line);
                        }

                        patternRows[pattern] = patternLines;
                    }
                }

                for (int j = 0; j < maxPatternCount; j++)
                {
                    lines.Add($"PATTERN {j:X2}");

                    for (int p = 0; p < song.PatternLength; p++)
                    {
                        var line = $"ROW {p:X2}";
                        for (int c = 0; c < song.Channels.Length; c++)
                        {
                            var channel = song.Channels[c];

                            if (j >= channel.Patterns.Count)
                            {
                                line += " : ... .. . ... ... ...";
                            }
                            else
                            {
                                line += patternRows[channel.Patterns[j]][p];
                            }
                        }

                        lines.Add(line);
                    }

                    lines.Add("");
                }
            }

            lines.Add("# End of export");

            File.WriteAllLines(filename, lines);

            return(true);
        }
Exemplo n.º 13
0
        public bool Save(Project originalProject, int songId, int loopCount, int colorMode, int numColumns, int lineThickness, string filename, int resX, int resY, bool halfFrameRate, int channelMask, int audioBitRate, int videoBitRate, bool stereo, float[] pan)
        {
            if (!Initialize(channelMask, loopCount))
            {
                return(false);
            }

            videoResX = resX;
            videoResY = resY;

            var project = originalProject.DeepClone();
            var song    = project.GetSong(songId);

            ExtendSongForLooping(song, loopCount);

            // Save audio to temporary file.
            Log.LogMessage(LogSeverity.Info, "Exporting audio...");

            var tempFolder    = Utils.GetTemporaryDiretory();
            var tempAudioFile = Path.Combine(tempFolder, "temp.wav");

            AudioExportUtils.Save(song, tempAudioFile, SampleRate, 1, -1, channelMask, false, false, stereo, pan, (samples, samplesChannels, fn) => { WaveFile.Save(samples, fn, SampleRate, samplesChannels); });

            // Start encoder, must be done before any GL calls on Android.
            GetFrameRateInfo(song.Project, halfFrameRate, out var frameRateNumer, out var frameRateDenom);

            if (!videoEncoder.BeginEncoding(videoResX, videoResY, frameRateNumer, frameRateDenom, videoBitRate, audioBitRate, tempAudioFile, filename))
            {
                Log.LogMessage(LogSeverity.Error, "Error starting video encoder, aborting.");
                return(false);
            }

            Log.LogMessage(LogSeverity.Info, "Initializing channels...");

            var numChannels        = Utils.NumberOfSetBits(channelMask);
            var longestChannelName = 0.0f;
            var videoGraphics      = RenderGraphics.Create(videoResX, videoResY, true);

            if (videoGraphics == null)
            {
                Log.LogMessage(LogSeverity.Error, "Error initializing off-screen graphics, aborting.");
                return(false);
            }

            var themeResources = new ThemeRenderResources(videoGraphics);
            var bmpWatermark   = videoGraphics.CreateBitmapFromResource("VideoWatermark");

            // Generate WAV data for each individual channel for the oscilloscope.
            var channelStates = new List <VideoChannelState>();
            var maxAbsSample  = 0;

            for (int i = 0, channelIndex = 0; i < song.Channels.Length; i++)
            {
                if ((channelMask & (1 << i)) == 0)
                {
                    continue;
                }

                var pattern = song.Channels[i].PatternInstances[0];
                var state   = new VideoChannelState();

                state.videoChannelIndex = channelIndex;
                state.songChannelIndex  = i;
                state.channel           = song.Channels[i];
                state.channelText       = state.channel.NameWithExpansion;
                state.wav = new WavPlayer(SampleRate, 1, 1 << i).GetSongSamples(song, song.Project.PalMode, -1);

                channelStates.Add(state);
                channelIndex++;

                // Find maximum absolute value to rescale the waveform.
                foreach (int s in state.wav)
                {
                    maxAbsSample = Math.Max(maxAbsSample, Math.Abs(s));
                }

                // Measure the longest text.
                longestChannelName = Math.Max(longestChannelName, videoGraphics.MeasureString(state.channelText, themeResources.FontVeryLarge));

                Log.ReportProgress(0.0f);
            }

            numColumns = Math.Min(numColumns, channelStates.Count);

            var numRows = (int)Math.Ceiling(channelStates.Count / (float)numColumns);

            var channelResXFloat = videoResX / (float)numColumns;
            var channelResYFloat = videoResY / (float)numRows;

            var channelResX = (int)channelResXFloat;
            var channelResY = (int)channelResYFloat;

            // Tweak some cosmetic stuff that depends on resolution.
            var smallChannelText = channelResY < 128;
            var bmpSuffix        = smallChannelText ? "" : "@2x";
            var font             = lineThickness > 1 ?
                                   (smallChannelText ? themeResources.FontMediumBold : themeResources.FontVeryLargeBold) :
                                   (smallChannelText ? themeResources.FontMedium     : themeResources.FontVeryLarge);
            var textOffsetY      = smallChannelText ? 1 : 4;
            var channelLineWidth = resY >= 720 ? 5 : 3;

            foreach (var s in channelStates)
            {
                s.bmpIcon = videoGraphics.CreateBitmapFromResource(ChannelType.Icons[s.channel.Type] + bmpSuffix);
            }

            var gradientSizeY = channelResY / 2;
            var gradientBrush = videoGraphics.CreateVerticalGradientBrush(0, gradientSizeY, Color.Black, Color.FromArgb(0, Color.Black));

            // Generate the metadata for the video so we know what's happening at every frame
            var metadata = new VideoMetadataPlayer(SampleRate, 1).GetVideoMetadata(song, song.Project.PalMode, -1);

            var oscScale      = maxAbsSample != 0 ? short.MaxValue / (float)maxAbsSample : 1.0f;
            var oscLookback   = (metadata[1].wavOffset - metadata[0].wavOffset) / 2;
            var oscWindowSize = (int)Math.Round(SampleRate * OscilloscopeWindowSize);

            BuildChannelColors(song, channelStates, metadata, colorMode);

            var videoImage   = new byte[videoResY * videoResX * 4];
            var oscilloscope = new float[oscWindowSize, 2];
            var success      = true;

#if !DEBUG
            try
#endif
            {
                // Generate each of the video frames.
                for (int f = 0; f < metadata.Length; f++)
                {
                    if (Log.ShouldAbortOperation)
                    {
                        success = false;
                        break;
                    }

                    if ((f % 100) == 0)
                    {
                        Log.LogMessage(LogSeverity.Info, $"Rendering frame {f} / {metadata.Length}");
                    }

                    Log.ReportProgress(f / (float)(metadata.Length - 1));

                    if (halfFrameRate && (f & 1) != 0)
                    {
                        continue;
                    }

                    var frame = metadata[f];

                    videoGraphics.BeginDrawFrame();
                    videoGraphics.BeginDrawControl(new Rectangle(0, 0, videoResX, videoResY), videoResY);
                    videoGraphics.Clear(Theme.DarkGreyLineColor2);

                    var cmd = videoGraphics.CreateCommandList();

                    // Draw gradients.
                    for (int i = 0; i < numRows; i++)
                    {
                        cmd.PushTranslation(0, i * channelResY);
                        cmd.FillRectangle(0, 0, videoResX, channelResY, gradientBrush);
                        cmd.PopTransform();
                    }

                    // Channel names + oscilloscope
                    for (int i = 0; i < channelStates.Count; i++)
                    {
                        var s = channelStates[i];

                        var channelX = i % numColumns;
                        var channelY = i / numColumns;

                        var channelPosX0 = (channelX + 0) * channelResX;
                        var channelPosX1 = (channelX + 1) * channelResX;
                        var channelPosY0 = (channelY + 0) * channelResY;
                        var channelPosY1 = (channelY + 1) * channelResY;

                        // Intentionally flipping min/max Y since D3D is upside down compared to how we display waves typically.
                        GenerateOscilloscope(s.wav, frame.wavOffset, oscWindowSize, oscLookback, oscScale, channelPosX0, channelPosY1, channelPosX1, channelPosY0, oscilloscope);

                        var brush = videoGraphics.GetSolidBrush(frame.channelColors[i]);

                        cmd.DrawGeometry(oscilloscope, brush, lineThickness, true);

                        var channelIconPosX = channelPosX0 + s.bmpIcon.Size.Width / 2;
                        var channelIconPosY = channelPosY0 + s.bmpIcon.Size.Height / 2;

                        cmd.FillAndDrawRectangle(channelIconPosX, channelIconPosY, channelIconPosX + s.bmpIcon.Size.Width - 1, channelIconPosY + s.bmpIcon.Size.Height - 1, themeResources.DarkGreyLineBrush2, themeResources.LightGreyFillBrush1);
                        cmd.DrawBitmap(s.bmpIcon, channelIconPosX, channelIconPosY, 1, Theme.LightGreyFillColor1);
                        cmd.DrawText(s.channelText, font, channelIconPosX + s.bmpIcon.Size.Width + ChannelIconTextSpacing, channelIconPosY + textOffsetY, themeResources.LightGreyFillBrush1);
                    }

                    // Grid lines
                    for (int i = 1; i < numRows; i++)
                    {
                        cmd.DrawLine(0, i * channelResY, videoResX, i * channelResY, themeResources.BlackBrush, channelLineWidth);
                    }
                    for (int i = 1; i < numColumns; i++)
                    {
                        cmd.DrawLine(i * channelResX, 0, i * channelResX, videoResY, themeResources.BlackBrush, channelLineWidth);
                    }

                    // Watermark.
                    cmd.DrawBitmap(bmpWatermark, videoResX - bmpWatermark.Size.Width, videoResY - bmpWatermark.Size.Height);
                    videoGraphics.DrawCommandList(cmd);
                    videoGraphics.EndDrawControl();
                    videoGraphics.EndDrawFrame();

                    // Readback
                    videoGraphics.GetBitmap(videoImage);

                    // Send to encoder.
                    videoEncoder.AddFrame(videoImage);
                }

                videoEncoder.EndEncoding(!success);

                File.Delete(tempAudioFile);
            }
#if !DEBUG
            catch (Exception e)
            {
                Log.LogMessage(LogSeverity.Error, "Error exporting video.");
                Log.LogMessage(LogSeverity.Error, e.Message);
            }
            finally
#endif
            {
                foreach (var c in channelStates)
                {
                    c.bmpIcon.Dispose();
                }

                themeResources.Dispose();
                bmpWatermark.Dispose();
                gradientBrush.Dispose();
                videoGraphics.Dispose();
            }

            return(success);
        }