/* 
  ============================================================================== 

   This file is part of the JUCE tutorials. 
   Copyright (c) 2017 - ROLI Ltd. 

   The code included in this file is provided under the terms of the ISC license 
   http://www.isc.org/downloads/software-support-policy/isc-license. Permission 
   To use, copy, modify, and/or distribute this software for any purpose with or 
   without fee is hereby granted provided that the above copyright notice and 
   this permission notice appear in all copies. 

   THE SOFTWARE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, 
   WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR 
   PURPOSE, ARE DISCLAIMED. 

  ============================================================================== 
*/ 

/******************************************************************************* 
 The block below describes the properties of this PIP. A PIP is a short snippet 
 of code that can be read by the Projucer and used to generate a JUCE project. 

 BEGIN_JUCE_PIP_METADATA 

 name:             ProcessingAudioInputTutorial 
 version:          1.0.0 
 vendor:           JUCE 
 website:          http://juce.com 
 description:      Performs processing on an input signal. 

 dependencies:     juce_audio_basics, juce_audio_devices, juce_audio_formats, 
                   juce_audio_processors, juce_audio_utils, juce_core, 
                   juce_data_structures, juce_events, juce_graphics, 
                   juce_gui_basics, juce_gui_extra 
 exporters:        xcode_mac, vs2017, linux_make 

 type:             Component 
 mainClass:        MainContentComponent 

 useLocalCopy:     1 

 END_JUCE_PIP_METADATA 

******************************************************************************* 
  
 Jonathan Feldman, May 2019 
  
 This version has the basical spectral drive algorithm working. 
  
 The incoming guitar gain is very low 
  
 The spectral components are being boosted but there is no distortion. 
  
 */ 

#pragma once 
#include  
#include  
using namespace std; 

//============================================================================== 
class MainContentComponent   : public AudioAppComponent, private Timer 

public: 
    //============================================================================== 
    MainContentComponent() : spectrogramFFT(fftOrder), 
                             algorithmFFT(fftOrder), 
                             spectrogramImage (Image::RGB, 512, 512, true) 
    { 
        // 
        // set parameter ranges for sliders 
        // 
        inputGainSlider.setRange (0, 10, 0.1); 
        ffSlider.setRange (0.0, 1.0, 0.1); 
        spreadSlider.setRange(1, 25, 0.5); 
        driveSlider.setRange (0, 250, 0.5); 

        // 
        // set up the sliders for display 
        // 

        inputGainSlider.setTextBoxStyle (Slider::TextBoxRight, false, 100, 20); 
        inputGainLabel.setText ("Input Gain", dontSendNotification); 

        ffSlider.setTextBoxStyle (Slider::TextBoxRight, false, 100, 20); 
        ffLabel.setText ("Floor Fraction", dontSendNotification); 

        spreadSlider.setTextBoxStyle (Slider::TextBoxRight, false, 100, 20); 
        spreadLabel.setText ("Spread", dontSendNotification); 

        driveSlider.setTextBoxStyle (Slider::TextBoxRight, false, 100, 20); 
        driveLabel.setText ("Drive", dontSendNotification); 

        //gainLabel.setText ("Max Input Gain", dontSendNotification); 
        //theGainLabel.setText ("Init", dontSendNotification); 

        //magnitudeLabel.setText ("Max FFT Magnitude", dontSendNotification); 
        //theMagnitudeLabel.setText ("Init", dontSendNotification); 

        addAndMakeVisible (inputGainSlider); 
        addAndMakeVisible (inputGainLabel); 
        addAndMakeVisible (ffSlider); 
        addAndMakeVisible (ffLabel); 
        addAndMakeVisible (spreadSlider); 
        addAndMakeVisible (spreadLabel); 
        addAndMakeVisible (driveSlider); 
        addAndMakeVisible (driveLabel); 
        //addAndMakeVisible (gainLabel); 
        //addAndMakeVisible (theGainLabel); 
        //addAndMakeVisible (magnitudeLabel); 
        //addAndMakeVisible (theMagnitudeLabel); 

        setSize (600, 100); 
         
        // 
        // set for mono input, mono output 
        // 
        // the guitar is coming in the left channel and going out the headphones 
        // which are connected to the left channel 
        // 
        // we will worry about stereo later 
        // 
         
        setAudioChannels (1, 1); 
         
        // 
        // set Spectrogram components 
        // 
        setOpaque (true); 
        startTimerHz (60); 
        setSize (700, 500); 

    } 

    ~MainContentComponent() 
    { 
        shutdownAudio(); 
    } 

    void prepareToPlay (int, double) override {} 

    void getNextAudioBlock (const AudioSourceChannelInfo& bufferToFill) override 
    { 
        auto* device = deviceManager.getCurrentAudioDevice(); 
        auto activeInputChannels  = device->getActiveInputChannels(); 
        auto activeOutputChannels = device->getActiveOutputChannels(); 
        auto maxInputChannels  = activeInputChannels .getHighestBit() + 1; 
        auto maxOutputChannels = activeOutputChannels.getHighestBit() + 1; 
         
        auto inputGainLevel = (float) inputGainSlider.getValue(); 
        auto ffLevel = (float) ffSlider.getValue(); 
        auto spreadLevel = (float) spreadSlider.getValue(); 
        auto driveLevel = (float) driveSlider.getValue(); 

        for (auto channel = 0; channel < maxOutputChannels; ++channel) 
        { 
            if ((! activeOutputChannels[channel]) || maxInputChannels == 0) 
            { 
                bufferToFill.buffer->clear (channel, bufferToFill.startSample, bufferToFill.numSamples); 
            } 
            else 
            { 
                auto actualInputChannel = channel % maxInputChannels; 
                 
                if (! activeInputChannels[channel]) 
                { 
                    bufferToFill.buffer->clear (channel, bufferToFill.startSample, bufferToFill.numSamples); 
                } 
                else // 
                { 
                    auto* inBuffer = bufferToFill.buffer->getReadPointer (actualInputChannel, bufferToFill.startSample); 
                     
                    //  we send the samples to the fifo to reverse the order of the samples 
                     
                     for (auto sample = 0; sample < bufferToFill.numSamples; ++sample) 
                     { 
                        sendBufferedDataToFifoForSpectrogram(inBuffer[sample]); 
                     } 
                     
                    // 
                    // reverse the order of the samples in the sample buffer to send to FFT 
                    // 
                     
                    for (auto sample = 0; sample < bufferToFill.numSamples; ++sample) 
                    { 
                        queuedSampleBuffer[sample] = inBuffer[fftSize-1-sample]; 
                    } 
                     
                    // 
                    // perform the forward FFT 
                    // 
                     
                    algorithmFFT.perform(queuedSampleBuffer, forwardFFTData, false); 
                     
                     
                    // 
                    // compute magnitude and phase information 
                    // 
                     
                     for (int i = 0; i < 2*fftSize; i+=2) 
                     { 
                        magnitude[i] = abs(forwardFFTData[i]); 
                         
                        if (real(forwardFFTData[i]) > 0) 
                        { 
                            phase[i] = atan(imag(forwardFFTData[i]) / real(forwardFFTData[i])); 
                        } 
                        else 
                        { 
                            phase[i] = M_PI + atan(imag(forwardFFTData[i]) / real(forwardFFTData[i])); 
                        } 
                     } 
                     
                     
                    // 
                    // The maximum amplitude of the incoming guitar signal is about 0.01, very low gain 
                    // 
                     
                    // 
                    // copy slider levels onto algorithm parameters 
                    // this is redundant code 
                    // 
                     
                     
                     floorFraction = ffLevel; 
                     spread = spreadLevel; 
                     drive = driveLevel; 
                     
                     for (int i = 0; i < 2*fftSize; i++) 
                     { 
                        if (magnitude[i] > floorFraction)  // 50 is some arbitrary FFT magnitude maximum 
                        { 
                            magnitude[i] = magnitude[i] * spread + drive; 
                        } 
                     } 
                     
                    // 
                    // reconstruct the Fourier data for the inverse transform 
                    // 
               
                     
                     zeromem(inverseFFTData, sizeof(inverseFFTData)); 
                     
                     for (int i = 0; i < 2*fftSize; i++) 
                     { 
                        float newReal = magnitude[i] * cos(phase[i]); 
                        float newComplex = magnitude[i] * sin(phase[i]); 
                         
                        complex c (newReal, newComplex); 
                         
                        fxFFTData[i] = c; 
                     } 
                     
                     algorithmFFT.perform(fxFFTData, inverseFFTData, true); 
                     
                     
                    // 
                    // we reverse the order of the samples coming out of the inverse fft 
                    // to send to the output buffer 
                    // 
                     
                    for (auto sample = 0; sample < bufferToFill.numSamples; ++sample) 
                    { 
                        outputSampleBuffer[sample] = real(inverseFFTData[fftSize-1-sample]); 
                    } 
                     
                    auto* outBuffer = bufferToFill.buffer->getWritePointer (channel, bufferToFill.startSample); 
                                                                           
                    for (auto sample = 0; sample < bufferToFill.numSamples; ++sample) 
                    { 
                        outBuffer[sample] = outputSampleBuffer[sample] * inputGainLevel; 
                    } 
                } 
            } 
        } 
    } 
     
    void sendBufferedDataToFifoForSpectrogram(float sample) noexcept 
    { 
        if (fifoIndex == fftSize) 
        { 
            if (!nextFFTBlockReady) 
            { 
                zeromem(forwardFFTData, sizeof(fftData)); 
                memcpy(forwardFFTData, fifo, sizeof(fifo)); 
                nextFFTBlockReady = true; 
            } 
             
            fifoIndex = 0; 
        } 
         
        fifo[fifoIndex++] = sample; 
    } 

    void releaseResources() override {} 

    void resized() override 
    { 
        inputGainLabel .setBounds (10, 10, 90, 20); 
        inputGainSlider.setBounds (100, 10, getWidth() - 110, 20); 

        ffLabel .setBounds (10, 40, 90, 20); 
        ffSlider.setBounds (100, 40, getWidth() - 110, 20); 

        spreadLabel .setBounds (10, 70, 90, 20); 
        spreadSlider.setBounds (100, 70, getWidth() - 110, 20); 
         
        driveLabel .setBounds (10, 100, 90, 20); 
        driveSlider.setBounds (100, 100, getWidth() - 110, 20); 

        gainLabel.setBounds (10, 130, 90, 20); 
        theGainLabel.setBounds (100, 130, 90, 20); 

        magnitudeLabel.setBounds (10, 160, 90, 20); 
        theMagnitudeLabel.setBounds (100, 160, 90, 20); 

        // 
        // this code should probably be called from the paint() function 
        // in order to use it here we need to resize the window 
        // 
        
        //auto gainString = String::formatted ("%f", inputGainTest); 
        //theGainLabel.setText (gainString, dontSendNotification); 

        //auto magnitudeString = String::formatted ("%f", fftMagTest); 
        //theMagnitudeLabel.setText (magnitudeString, dontSendNotification); 

    } 
     
    void paint (Graphics& g) override 
    { 
        g.fillAll (Colours::black); 
        g.setOpacity (1.0f); 
        g.drawImage (spectrogramImage, getLocalBounds().toFloat()); 
    } 
     
    void timerCallback() override 
    { 
        if (nextFFTBlockReady) 
        { 
            drawNextLineOfSpectrogram(); 
            nextFFTBlockReady = false; 
            repaint(); 
        } 
    } 

    void drawNextLineOfSpectrogram() 
    { 
        auto rightHandEdge = spectrogramImage.getWidth() - 1; 
        auto imageHeight   = spectrogramImage.getHeight(); 
         
        // first, shuffle our image leftwards by 1 pixel.. 
        spectrogramImage.moveImageSection (0, 0, 1, 0, rightHandEdge, imageHeight); 
         
        // 
        // performFrequencyOnlyForwardTransform works here with fftData, the spectrogram is clean 
        // 
        // spectrogramFFT.performFrequencyOnlyForwardTransform(fftData); 
        // 
         
        // 
        // now try using a complex valued FFT 
        // 
         
        spectrogramFFT.perform(fifo /*input*/, fftData /*output*/,false); 
         
        for (int i = 0; i < fftSize; i++) 
        { 
            magnitude[i] = abs(fftData[i]); 
        } 
         
        // find the range of values produced, so we can scale our rendering to 
        // show up the detail clearly 
        auto maxLevel = FloatVectorOperations::findMinAndMax (magnitude, fftSize); 
         
        for (auto y = 1; y < imageHeight; ++y) 
        { 
            // auto skewedProportionY = 1.0f - std::exp (std::log (y / (float) imageHeight) * 0.2f); 
            // auto fftDataIndex = jlimit (0, fftSize, (int) (skewedProportionY * fftSize)); 
            // auto level = jmap (fftDataForward[fftDataIndex], 0.0f, jmax (maxLevel.getEnd(), 1e-5f), 0.0f, 1.0f); 

            auto skewedProportionY = 1.0f - std::exp (std::log (y / (float) imageHeight) * 0.2f); 
            auto a = fftSize; 
            auto b = (int) (skewedProportionY * fftSize); 
            auto c = b < a ? b : a; 
            auto fftDataIndex = jmax (0, c); 
            auto level = jmap (magnitude[fftDataIndex], 0.0f, jmax (maxLevel.getEnd(), 1e-5f), 0.0f, 1.0f); 

            spectrogramImage.setPixelAt (rightHandEdge, y, Colour::fromHSV (level, 1.0f, level, 1.0f)); 
        } 
    } 

    enum 
    { 
        fftOrder = 9, 
        fftSize = 1 << fftOrder 
    }; 

private: 
     
    // 
    // UI variables 
    // 
     
    Slider inputGainSlider; 
    Label inputGainLabel; 

    Slider ffSlider; 
    Label ffLabel; 
     
    Slider spreadSlider; 
    Label spreadLabel; 

    Slider driveSlider; 
    Label driveLabel; 

    Label gainLabel; 
    Label theGainLabel; 

    Label magnitudeLabel; 
    Label theMagnitudeLabel; 

    //float inputGainTest = 0.0; 
    //float fftMagTest = 0.0; 
     
    // 
    // signal processing variables 
    // 
     
    Random random; 

    dsp::FFT spectrogramFFT; 
    dsp::FFT algorithmFFT; 
     
    complex queuedSampleBuffer[fftSize]; 
    float outputSampleBuffer[fftSize]; 
     
    complex forwardFFTData[fftSize*2]; 
    complex fxFFTData[fftSize*2]; 
    complex inverseFFTData[fftSize*2]; 
     
    float magnitude[fftSize*2]; 
    float phase[fftSize*2]; 
     
    float floorFraction; 
    float spread; 
    float drive; 

    // 
    // Spectrogram Image 
    // 
     
    Image spectrogramImage; 

    complex fifo[fftSize]; 
    complex fftData[2*fftSize]; 
    int fifoIndex = 0; 
    bool nextFFTBlockReady = false; 

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainContentComponent) 
};