/* -LICENSE-START-
** Copyright (c) 2009 Blackmagic Design
**
** Permission is hereby granted, free of charge, to any person or organization
** obtaining a copy of the software and accompanying documentation covered by
** this license (the "Software") to use, reproduce, display, distribute,
** execute, and transmit the Software, and to prepare derivative works of the
** Software, and to permit third-parties to whom the Software is furnished to
** do so, all subject to the following:
** 
** The copyright notices in the Software and this entire statement, including
** the above license grant, this restriction and the following disclaimer,
** must be included in all copies of the Software, in whole or in part, and
** all derivative works of the Software, unless such copies or derivative
** works are solely in the form of machine-executable object code generated by
** a source language processor.
** 
** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
** DEALINGS IN THE SOFTWARE.
** -LICENSE-END-
*/
using System;
using System.Windows.Forms;

using System.Runtime.InteropServices;
using DeckLinkAPI;

namespace SignalGenCSharp
{
    public partial class SignalGenerator : Form, IDeckLinkVideoOutputCallback, IDeckLinkAudioOutputCallback
    {
        enum OutputSignal
        {
            kOutputSignalPip = 0,
            kOutputSignalDrop = 1
        };
        const uint kAudioWaterlevel = 48000;

        private bool m_running;
        private IDeckLink m_deckLink;
        private IDeckLinkOutput m_deckLinkOutput;
        //
        private int m_frameWidth;
        private int m_frameHeight;
        private long m_frameDuration;
        private long m_frameTimescale;
        private uint m_framesPerSecond;
        private IDeckLinkMutableVideoFrame m_videoFrameBlack;
        private IDeckLinkMutableVideoFrame m_videoFrameBars;
        private uint m_totalFramesScheduled;
        //
        private OutputSignal m_outputSignal;
        private IntPtr m_audioBuffer;
        private uint m_audioBufferOffset;
        private uint m_audioBufferSampleLength;
        private uint m_audioChannelCount;
        private _BMDAudioSampleRate m_audioSampleRate;
        private _BMDAudioSampleType m_audioSampleDepth;

        public SignalGenerator()
        {
            InitializeComponent();

            m_running = false;

            // Create the COM instance
            IDeckLinkIterator deckLinkIterator = new CDeckLinkIterator();
            if (deckLinkIterator == null)
            {
                MessageBox.Show("This application requires the DeckLink drivers installed.\nPlease install the Blackmagic DeckLink drivers to use the features of this application", "Error");
                Environment.Exit(1);
            }

            // Get the first DeckLink card
            deckLinkIterator.Next(out m_deckLink);
            if (m_deckLink == null)
            {
                MessageBox.Show("This application requires a DeckLink PCI card.\nYou will not be able to use the features of this application until a DeckLink PCI card is installed.", "Error");
                Environment.Exit(1);
            }

            // Get the IDeckLinkOutput interface
            m_deckLinkOutput = (IDeckLinkOutput)m_deckLink;

            // Provide this class as a delegate to the audio and video output interfaces
            m_deckLinkOutput.SetScheduledFrameCompletionCallback(this);
            m_deckLinkOutput.SetAudioCallback(this);
	
	        // Populate the display mode combo with a list of display modes supported by the installed DeckLink card
	        IDeckLinkDisplayModeIterator		displayModeIterator;

        	comboBoxVideoFormat.BeginUpdate();
            comboBoxVideoFormat.Items.Clear();

            m_deckLinkOutput.GetDisplayModeIterator(out displayModeIterator);

            while (true)
            {
                IDeckLinkDisplayMode deckLinkDisplayMode;

                displayModeIterator.Next(out deckLinkDisplayMode);
                if (deckLinkDisplayMode == null)
                    break;

                comboBoxVideoFormat.Items.Add(new DisplayModeEntry(deckLinkDisplayMode));
            }

            comboBoxVideoFormat.EndUpdate();

            // Output signal combo box
            comboBoxOutputSignal.BeginUpdate();
            comboBoxOutputSignal.Items.Clear();
            comboBoxOutputSignal.Items.Add(new StringObjectPair<OutputSignal>("Pip", OutputSignal.kOutputSignalPip));
            comboBoxOutputSignal.Items.Add(new StringObjectPair<OutputSignal>("Drop", OutputSignal.kOutputSignalDrop));
            comboBoxOutputSignal.EndUpdate();

            // Audio channels combo box
            comboBoxAudioChannels.BeginUpdate();
            comboBoxAudioChannels.Items.Clear();
            comboBoxAudioChannels.Items.Add("2");
            comboBoxAudioChannels.Items.Add("8");
            comboBoxAudioChannels.Items.Add("16");
            comboBoxAudioChannels.EndUpdate();

            // Audio depth combo box
            comboBoxAudioDepth.BeginUpdate();
            comboBoxAudioDepth.Items.Clear();
            comboBoxAudioDepth.Items.Add(new StringObjectPair<_BMDAudioSampleType>("16 Bit", _BMDAudioSampleType.bmdAudioSampleType16bitInteger));
            comboBoxAudioDepth.Items.Add(new StringObjectPair<_BMDAudioSampleType>("32 Bit", _BMDAudioSampleType.bmdAudioSampleType32bitInteger));
            comboBoxAudioDepth.EndUpdate();

            comboBoxVideoFormat.SelectedIndex = 0;
            comboBoxOutputSignal.SelectedIndex = 0;
            comboBoxAudioChannels.SelectedIndex = 0;
            comboBoxAudioDepth.SelectedIndex = 0;
        }

        private void buttonStartStop_Click(object sender, EventArgs e)
        {
            if (m_running)
                StopRunning();
            else
                StartRunning();
        }

        private void StartRunning()
        {
            m_outputSignal = ((StringObjectPair<OutputSignal>)comboBoxOutputSignal.SelectedItem).value;
            m_audioChannelCount = uint.Parse((string)comboBoxAudioChannels.SelectedItem);
            m_audioSampleDepth = ((StringObjectPair<_BMDAudioSampleType>)comboBoxAudioDepth.SelectedItem).value;
            m_audioSampleRate = _BMDAudioSampleRate.bmdAudioSampleRate48kHz;
            //
            //- Extract the IDeckLinkDisplayMode from the display mode popup menu
            IDeckLinkDisplayMode videoDisplayMode;
            videoDisplayMode = ((DisplayModeEntry)comboBoxVideoFormat.SelectedItem).displayMode;
            m_frameWidth = videoDisplayMode.GetWidth();
            m_frameHeight = videoDisplayMode.GetHeight();
            videoDisplayMode.GetFrameRate(out m_frameDuration, out m_frameTimescale);
            // Calculate the number of frames per second, rounded up to the nearest integer.  For example, for NTSC (29.97 FPS), framesPerSecond == 30.
            m_framesPerSecond = (uint)((m_frameTimescale + (m_frameDuration-1))  /  m_frameDuration);

            // Set the video output mode
            m_deckLinkOutput.EnableVideoOutput(videoDisplayMode.GetDisplayMode(), _BMDVideoOutputFlags.bmdVideoOutputFlagDefault);

            // Set the audio output mode
            m_deckLinkOutput.EnableAudioOutput(m_audioSampleRate, m_audioSampleDepth, m_audioChannelCount, _BMDAudioOutputStreamType.bmdAudioOutputStreamContinuous);

            // Generate one second of audio
            m_audioBufferSampleLength = (uint)((m_framesPerSecond * (uint)m_audioSampleRate * m_frameDuration) / m_frameTimescale);
            m_audioBuffer = Marshal.AllocCoTaskMem((int)(m_audioBufferSampleLength * m_audioChannelCount * ((uint)m_audioSampleDepth / 8)));

	        // Zero the buffer (interpreted as audio silence)
            for (int i = 0; i < (m_audioBufferSampleLength * m_audioChannelCount * (uint)m_audioSampleDepth / 8); i++)
                Marshal.WriteInt32(m_audioBuffer, i, 0);
	        uint audioSamplesPerFrame = (uint)(((uint)m_audioSampleRate * m_frameDuration) / m_frameTimescale);

            if (m_outputSignal == OutputSignal.kOutputSignalPip)
                FillSine(m_audioBuffer, audioSamplesPerFrame, m_audioChannelCount, m_audioSampleDepth);
            else
                FillSine(new IntPtr(m_audioBuffer.ToInt64() + (audioSamplesPerFrame * m_audioChannelCount * (uint)m_audioSampleDepth / 8)), (m_audioBufferSampleLength - audioSamplesPerFrame), m_audioChannelCount, m_audioSampleDepth);

            // Generate a frame of black
            m_deckLinkOutput.CreateVideoFrame(m_frameWidth, m_frameHeight, m_frameWidth * 2, _BMDPixelFormat.bmdFormat8BitYUV, _BMDFrameFlags.bmdFrameFlagDefault, out m_videoFrameBlack);
            FillBlack(m_videoFrameBlack);

            // Generate a frame of colour bars
            m_deckLinkOutput.CreateVideoFrame(m_frameWidth, m_frameHeight, m_frameWidth * 2, _BMDPixelFormat.bmdFormat8BitYUV, _BMDFrameFlags.bmdFrameFlagDefault, out m_videoFrameBars);
            FillColourBars(m_videoFrameBars);

            // Begin video preroll by scheduling a second of frames in hardware
            m_totalFramesScheduled = 0;
            for (uint i = 0; i < m_framesPerSecond; i++)
                ScheduleNextFrame(true);

            // Begin audio preroll.  This will begin calling our audio callback, which will start the DeckLink output stream.
            m_audioBufferOffset = 0;
            m_deckLinkOutput.BeginAudioPreroll();

            m_running = true;
            buttonStartStop.Text = "Stop";
        }

        private void StopRunning()
        {
            long unused;
            m_deckLinkOutput.StopScheduledPlayback(0, out unused, 100);
            m_deckLinkOutput.DisableAudioOutput();
            m_deckLinkOutput.DisableVideoOutput();

            // free audio buffer
            Marshal.FreeCoTaskMem(m_audioBuffer);

            m_running = false;
            buttonStartStop.Text = "Start";
        }

        private void ScheduleNextFrame(bool prerolling)
        {
            if (prerolling == false)
            {
                // If not prerolling, make sure that playback is still active
                if (m_running == false)
                    return;
            }

            if (m_outputSignal == OutputSignal.kOutputSignalPip)
            {
                if ((m_totalFramesScheduled % m_framesPerSecond) == 0)
                {
                    // On each second, schedule a frame of bars
                    m_deckLinkOutput.ScheduleVideoFrame(m_videoFrameBars, (m_totalFramesScheduled * m_frameDuration), m_frameDuration, m_frameTimescale);
                }
                else
                {
                    // Schedue frames of black
                    m_deckLinkOutput.ScheduleVideoFrame(m_videoFrameBlack, (m_totalFramesScheduled * m_frameDuration), m_frameDuration, m_frameTimescale);
                }
            }
            else
            {
                if ((m_totalFramesScheduled % m_framesPerSecond) == 0)
                {
                    // On each second, schedule a frame of black
                    m_deckLinkOutput.ScheduleVideoFrame(m_videoFrameBlack, (m_totalFramesScheduled * m_frameDuration), m_frameDuration, m_frameTimescale);
                }
                else
                {
                    // Schedue frames of color bars
                    m_deckLinkOutput.ScheduleVideoFrame(m_videoFrameBars, (m_totalFramesScheduled * m_frameDuration), m_frameDuration, m_frameTimescale);
                }
            }

            m_totalFramesScheduled += 1;
        }

        void WriteNextAudioSamples()
        {
            // Write one second of audio to the DeckLink API.
            uint bufferedSamples;
	
	        // Try to maintain the number of audio samples buffered in the API at a specified waterlevel
            m_deckLinkOutput.GetBufferedAudioSampleFrameCount(out bufferedSamples);
            if (bufferedSamples < kAudioWaterlevel)
            {
                uint samplesToEndOfBuffer;
                uint samplesToWrite;
                uint samplesWritten;

                samplesToEndOfBuffer = (m_audioBufferSampleLength - m_audioBufferOffset);
                samplesToWrite = (kAudioWaterlevel - bufferedSamples);
                if (samplesToWrite > samplesToEndOfBuffer)
                    samplesToWrite = samplesToEndOfBuffer;

                m_deckLinkOutput.ScheduleAudioSamples(new IntPtr(m_audioBuffer.ToInt64() + (m_audioBufferOffset * m_audioChannelCount * (uint)m_audioSampleDepth / 8)),
                    samplesToWrite, 0, 0, out samplesWritten);
                m_audioBufferOffset = ((m_audioBufferOffset + samplesWritten) % m_audioBufferSampleLength);
            }
        }

        #region callbacks
        // Explicit implementation of IDeckLinkVideoOutputCallback and IDeckLinkAudioOutputCallback
        void IDeckLinkVideoOutputCallback.ScheduledFrameCompleted(IDeckLinkVideoFrame completedFrame, _BMDOutputFrameCompletionResult result)
        {
            // Note: if you throw an exception, it will be ignored by the caller.

            // When a frame has been completed
            ScheduleNextFrame(false);
        }

        void IDeckLinkVideoOutputCallback.ScheduledPlaybackHasStopped()
        {
        }

        void IDeckLinkAudioOutputCallback.RenderAudioSamples(int preroll)
        {
            // Note: if you throw an exception, it will be ignored by the caller.

            // Provide further audio samples to the DeckLink API until our preferred buffer waterlevel is reached
            WriteNextAudioSamples();

            if (preroll != 0)
            {
                m_deckLinkOutput.StartScheduledPlayback(0, 100, 1.0);
            }
        }
        #endregion

        #region buffer filling
        /*****************************************/

        void FillSine(IntPtr audioBuffer, uint samplesToWrite, uint channels, _BMDAudioSampleType sampleDepth)
        {
            if ((uint)sampleDepth == 16)
            {
                Int16[] buffer = new Int16[channels * samplesToWrite];

                for (uint i = 0; i < samplesToWrite; i++)
                {
                    Int16 sample = (Int16)(24576.0 * Math.Sin((i * 2.0 * Math.PI) / 48.0));
                    for (uint ch = 0; ch < channels; ch++)
                    {
                        buffer[i * channels + ch] = sample;
                    }
                }
                // Copy it into unmanaged buffer
                Marshal.Copy(buffer, 0, audioBuffer, (int)(channels * samplesToWrite));
            }
            else if ((uint)sampleDepth == 32)
            {
                Int32[] buffer = new Int32[channels * samplesToWrite];

                for (uint i = 0; i < samplesToWrite; i++)
                {
                    Int32 sample = (Int32)(1610612736.0 * Math.Sin((i * 2.0 * Math.PI) / 48.0));
                    for (uint ch = 0; ch < channels; ch++)
                    {
                        buffer[i * channels + ch] = sample;
                    }
                }
                // Copy it into unmanaged buffer
                Marshal.Copy(buffer, 0, audioBuffer, (int)(channels * samplesToWrite));

            }
        }

        void FillColourBars(IDeckLinkVideoFrame theFrame)
        {
            IntPtr          buffer;
            int             width, height;
            UInt32[]        bars = {0xEA80EA80, 0xD292D210, 0xA910A9A5, 0x90229035, 0x6ADD6ACA, 0x51EF515A, 0x286D28EF, 0x10801080};
            int             index = 0;

            theFrame.GetBytes(out buffer);
            width = theFrame.GetWidth();
            height = theFrame.GetHeight();

            for (uint y = 0; y < height; y++)
            {
                for (uint x = 0; x < width; x += 2)
                {
                    // Write directly into unmanaged buffer
                    Marshal.WriteInt32(buffer, index * 4, (Int32)bars[(x * 8) / width]);
                    index++;
                }
            }
        }

        void FillBlack(IDeckLinkVideoFrame theFrame)
        {
            IntPtr buffer;
            int             width, height;
            int             wordsRemaining;
            UInt32          black = 0x10801080;
            int             index = 0;

            theFrame.GetBytes(out buffer);
            width = theFrame.GetWidth();
            height = theFrame.GetHeight();

            wordsRemaining = (width * 2 * height) / 4;

            while (wordsRemaining-- > 0)
            {
                Marshal.WriteInt32(buffer, index*4, (Int32)black);
                index++;
            }
        }

        #endregion

        /// <summary>
        /// Used for putting the IDeckLinkDisplayMode objects into the video format
        /// combo box.
        /// </summary>
        struct DisplayModeEntry
        {
            public IDeckLinkDisplayMode displayMode;

            public DisplayModeEntry(IDeckLinkDisplayMode displayMode)
            {
                this.displayMode = displayMode;
            }

            public override string ToString()
            {
                string str;

                displayMode.GetName(out str);

                return str;
            }
        }

        /// <summary>
        /// Used for putting other object types into combo boxes.
        /// </summary>
        struct StringObjectPair<T>
        {
            public string name;
            public T value;

            public StringObjectPair(string name, T value)
            {
                this.name = name;
                this.value = value;
            }

            public override string ToString()
            {
                return name;
            }
        }
    }
}
