Sine Wave Synth as Plugin

Hi folks,

I’m trying to recreate the Sine Wave Synth tutorial found here:

https://docs.juce.com/master/tutorial_sine_synth.html

but as a plugin. I’m encountering two distinct issues:

1.) When I try to loop through the output channels using the processBlock template code provided by JUCE, I don’t hear a smooth sine-wave, but rather a distorted garble. I suspect that this is because I’m creating a waveform discontinuity between iterations of the outer, output-channel for-loop:

void updateAngleDelta()
{

    auto cyclesPerSample = freq / currentSampleRate;
    angleDelta = cyclesPerSample * 2.0 + MathConstants<double>::pi;
}

void prepareToPlay (double sampleRate, int samplesPerBlock) override
{
    currentSampleRate = sampleRate;
    updateAngleDelta();
}

void processBlock (AudioBuffer<float>& buffer, MidiBuffer&) override
{
   // ScopedNoDenormals noDenormals;
    auto totalNumInputChannels  = getTotalNumInputChannels();
    auto totalNumOutputChannels = getTotalNumOutputChannels();
    
    for(auto i = 0; i < totalNumOutputChannels; i++)
    {
        buffer.clear (i, 0, buffer.getNumSamples());
        
        auto* channelData = buffer.getWritePointer (i);
     
        for (auto sample = 0; sample < buffer.getNumSamples(); sample++)
        {
    
            channelData[sample] = (float) std::sin(currentAngle) * 0.5;
            currentAngle += angleDelta;
            
        }
        
        //Added this line to correct for sequential incrementing in outer for-loop, but sound artifacts persist...
        currentAngle -= angleDelta * buffer.getNumSamples();
    }
    
}

2.) When I attempt to manually access each output channel with separate getWritePointer calls, I don’t hear anything. I am 99% sure that I have exactly 2 output channels (I tried using the above code and only writing to the left and right channels respectively, and I only hear output in my left/right speakers), but for some reason when I try to manually create WritePointers with params 0, 1, I can’t seem to get output:

void updateAngleDelta()
{

    auto cyclesPerSample = freq / currentSampleRate;
    angleDelta = cyclesPerSample * 2.0 + MathConstants<double>::pi;
}

void prepareToPlay (double sampleRate, int samplesPerBlock) override
{
    currentSampleRate = sampleRate;
    updateAngleDelta();
}

void processBlock (AudioBuffer<float>& buffer, MidiBuffer&) override
{
   // ScopedNoDenormals noDenormals;
    auto totalNumInputChannels  = getTotalNumInputChannels();
    auto totalNumOutputChannels = getTotalNumOutputChannels();
    
   // for(auto i = 0; i < totalNumOutputChannels; i++)
    //{
        buffer.clear (0, 0, buffer.getNumSamples());
        buffer.clear (1, 0, buffer.getNumSamples());
        
        auto* channelDataL = buffer.getWritePointer (0);
        auto* channelDataR = buffer.getWritePointer(1);
     
        for (auto sample = 0; sample < buffer.getNumSamples(); sample++)
        {
            
            channelDataL[sample] = (float) std::sin(currentAngle) * 0.5;
            channelDataR[sample] = (float) std::sin(currentAngle) * 0.5;
            currentAngle += angleDelta;
            
        }
    
   // }
    
}

So to summarize, two-part question:
1.) Is my logic wrong in example 1?
2.) In example 2, is there a reason that in a plugin context I can’t manually create writePointers for each output Buffer rather than using a for-loop?

Thanks very much!

It’s probably this common issue, each channel needs it’s own state. In #1 you are using the same currentAngle for both channels. #1 most common programming mistake that we see on the forum

You are trying to handle this by subtracting from currentAngle, but you should only do this after the first channel. (But really each channel should have state)

Thank you for the reply.

This is why I was trying to create two separate writePointers in part 2 of my question. Alternately, I tried this, using a vector with size set by a getNumOutputs call in the constructor, but this also produces no sound:

void processBlock (AudioBuffer<float>& buffer, MidiBuffer&) override
{
   // ScopedNoDenormals noDenormals;
    auto totalNumInputChannels  = getTotalNumInputChannels();
    auto totalNumOutputChannels = getTotalNumOutputChannels();
    
    for(auto i = 0; i < totalNumOutputChannels; i++)
    {
     
        buffer.clear(i, 0, buffer.getNumSamples());
        
        auto* channelData = buffer.getWritePointer(i);
     
        for (auto sample = 0; sample < buffer.getNumSamples(); sample++)
        {
            
            channelData[sample] = (float) std:: sin (currentAngles[i]);
            currentAngles[i] += angleDelta;

        }
        
    }
    
}

Just for simplicity, here’s the same thing, without the vector, also produces no output:

void processBlock (AudioBuffer<float>& buffer, MidiBuffer&) override
{
   // ScopedNoDenormals noDenormals;
    auto totalNumInputChannels  = getTotalNumInputChannels();
    auto totalNumOutputChannels = getTotalNumOutputChannels();
    
    for(auto i = 0; i < totalNumOutputChannels; i++)
    {
     
        buffer.clear(i, 0, buffer.getNumSamples());
        
        auto* channelData = buffer.getWritePointer(i);
     
        for (auto sample = 0; sample < buffer.getNumSamples(); sample++)
        {
            
            if (i == 0){
            channelData[sample] = (float) std:: sin(currentAngleL);
            currentAngleL += angleDelta;
            }
            
            else{
                channelData[sample] = (float) std:: sin(currentAngleR);
                currentAngleR += angleDelta;
            }
            
           // channelData[sample] = (float) std:: sin (currentAngles[i]);
           // currentAngles[i] += angleDelta;

        }
        
    }
    
}

Wanted to let folks know I found the issue:

In the updateAngleDelta call there is a *2 carried over from the original demo, but because in this case each channel is being processed separately, the *2 is causing a phase cancellation in the sine function!

Deleting it solves the problem!