I have made a simple program that outputs 8 LFOs, 4.096Vpp + 2.048V (DC), to keep it within the Bela's usable input's range. However, when these are plugged back into the inputs, the waveform is cut at about half the amplitude.

Furthermore, without anything connected to them, the maximum DC voltage at my inputs is about 9V, give or take.

I am attaching my scope, which shows

Blue: the LFO, directly sent from my code (scaled by 1/4.096 to keep max at 1):
Red: LFO back into input
Green: 2nd analog input, which with pots turn to maximum shows 0.9006V

alt text

#include <Bela.h>
#include <cmath>
#include <libraries/Scope/Scope.h>
#include <iostream>

Scope scope;

int gAudioFramesPerAnalogFrame = 0;
float gInverseSampleRate;
float gPhase1, gPhase2, gPhase3, gPhase4, gPhase5, gPhase6, gPhase7, gPhase8;

float gAmplitude = 2.048;
float gFrequency = 0.1;
float DCamplitude = 2.048;

float gIn1, gIn2, gIn3, gIn4, gIn5, gIn6, gIn7, gIn8;
float LFO1, LFO2, LFO3, LFO4, LFO5, LFO6, LFO7, LFO8;

// For this example you need to set the Analog Sample Rate to
// 44.1 KHz which you can do in the settings tab.

bool setup(BelaContext *context, void *userData)
{

	// setup the scope with 3 channels at the audio sample rate
	scope.setup(16, context->audioSampleRate);

	if(context->analogSampleRate > context->audioSampleRate)
	{
		fprintf(stderr, "Error: for this project the sampling rate of the analog inputs has to be <= the audio sample rate\n");
		return false;
	}
	if(context->analogInChannels < 2)
	{
		fprintf(stderr, "Error: for this project you need at least two analog inputs\n");
		return false;
	}

	if(context->analogFrames)
		gAudioFramesPerAnalogFrame = context->audioFrames / context->analogFrames;
		
	gInverseSampleRate = 1.0 / context->audioSampleRate;

	return true;
}

void render(BelaContext *context, void *userData)
{

	for(unsigned int n = 0; n < context->audioFrames; n++) 
	{

			
				// generate a sine wave with the amplitude and frequency
			
			LFO1 = gAmplitude * sinf(gPhase1) + DCamplitude;
			gPhase1 += 2.0f * (float)M_PI * gFrequency * gInverseSampleRate;
			if(gPhase1 > M_PI)
			{
				gPhase1 -= 2.0f * (float)M_PI;
			}
			
			LFO2 = gAmplitude * sinf(gPhase2) + DCamplitude;
			gPhase2 += 2.0f * (float)M_PI * gFrequency * 2 * gInverseSampleRate;
			if(gPhase2 > M_PI)
			{
				gPhase2 -= 2.0f * (float)M_PI;
			}
			
			LFO3 = gAmplitude * sinf(gPhase3) + DCamplitude;
			gPhase3 += 2.0f * (float)M_PI * gFrequency * 4 * gInverseSampleRate;
			if(gPhase3 > M_PI)
			{
				gPhase3 -= 2.0f * (float)M_PI;
			}
			
			LFO4 = gAmplitude * sinf(gPhase4) + DCamplitude;
			gPhase4 += 2.0f * (float)M_PI * gFrequency * 8 * gInverseSampleRate;
			if(gPhase4 > M_PI)
			{
				gPhase4 -= 2.0f * (float)M_PI;
			}
			
			LFO5 = gAmplitude * sinf(gPhase5) + DCamplitude;
			gPhase5 += 2.0f * (float)M_PI * gFrequency * 16 * gInverseSampleRate;
			if(gPhase5 > M_PI)
			{
				gPhase5 -= 2.0f * (float)M_PI;
			}
			
			LFO6 = gAmplitude * sinf(gPhase6) + DCamplitude;
			gPhase6 += 2.0f * (float)M_PI * gFrequency * 32 *gInverseSampleRate;
			if(gPhase6 > M_PI)
			{
				gPhase6 -= 2.0f * (float)M_PI;
			}
			
			LFO7 = gAmplitude * sinf(gPhase7) + DCamplitude;
			gPhase7 += 2.0f * (float)M_PI * gFrequency * 64 * gInverseSampleRate;
			if(gPhase7 > M_PI)
			{
				gPhase7 -= 2.0f * (float)M_PI + DCamplitude;
			}
			
			LFO8 = gAmplitude * sinf(gPhase8) + DCamplitude;
			gPhase8 += 2.0f * (float)M_PI * gFrequency * 128 * gInverseSampleRate;
			if(gPhase8 > M_PI)
			{
				gPhase8 -= 2.0f * (float)M_PI;
			}
			

			gIn1 = analogRead(context, n/gAudioFramesPerAnalogFrame, 0);
			gIn2 = analogRead(context, n/gAudioFramesPerAnalogFrame, 1);
			gIn3 = analogRead(context, n/gAudioFramesPerAnalogFrame, 2);
			gIn4 = analogRead(context, n/gAudioFramesPerAnalogFrame, 3);
			gIn5 = analogRead(context, n/gAudioFramesPerAnalogFrame, 4);
			gIn6 = analogRead(context, n/gAudioFramesPerAnalogFrame, 5);
			gIn7 = analogRead(context, n/gAudioFramesPerAnalogFrame, 6);
			gIn8 = analogRead(context, n/gAudioFramesPerAnalogFrame, 7);
			
			analogWriteOnce(context, n/2, 0, LFO1);
			analogWriteOnce(context, n/gAudioFramesPerAnalogFrame, 1, LFO1);
			analogWriteOnce(context, n/gAudioFramesPerAnalogFrame, 2, LFO3);
			analogWriteOnce(context, n/gAudioFramesPerAnalogFrame, 3, LFO4);
			analogWriteOnce(context, n/gAudioFramesPerAnalogFrame, 4, LFO5);
			analogWriteOnce(context, n/gAudioFramesPerAnalogFrame, 5, LFO6);
			analogWriteOnce(context, n/gAudioFramesPerAnalogFrame, 6, LFO7);
			analogWriteOnce(context, n/gAudioFramesPerAnalogFrame, 7, LFO8);
			
			
			
		}
		
		
		



		// log the sine wave and sensor values on the scope
		scope.log(gIn1, LFO8, gIn2);

}

void cleanup(BelaContext *context, void *userData)
{

}

Edit: Furthermore, without anything connected to them, the maximum DC voltage at my inputs is about 0.9V, give or take.

    PLEASE allow me to refactor your code into something more readable:

    #include <Bela.h>
    #include <cmath>
    #include <libraries/Scope/Scope.h>
    #include <iostream>
    #include <libraries/Oscillator/Oscillator.h>
    #include <vector>
    
    Scope scope;
    
    int gAudioFramesPerAnalogFrame = 0;
    float gInverseSampleRate;
    
    const size_t kNumOscs = 8;
    std::vector<Oscillator> gOscs;
    std::vector<float> gLFOs(kNumOscs);
    std::vector<float> gIns(kNumOscs);
    
    float gAmplitude = 2.048;
    float gFrequency = 0.1;
    float DCamplitude = 2.048;
    
    // For this example you need to set the Analog Sample Rate to
    // 44.1 KHz which you can do in the settings tab.
    
    bool setup(BelaContext *context, void *userData)
    {
    	for(unsigned int n = 0; n < 8; ++n)
    		gOscs.push_back({context->audioSampleRate});
    
    	// setup the scope with 3 channels at the audio sample rate
    	scope.setup(3, context->audioSampleRate);
    
    	if(context->analogSampleRate > context->audioSampleRate)
    	{
    		fprintf(stderr, "Error: for this project the sampling rate of the analog inputs has to be <= the audio sample rate\n");
    		return false;
    	}
    	if(context->analogInChannels < 2)
    	{
    		fprintf(stderr, "Error: for this project you need at least two analog inputs\n");
    		return false;
    	}
    
    	if(context->analogFrames)
    		gAudioFramesPerAnalogFrame = context->audioFrames / context->analogFrames;
    	gInverseSampleRate = 1.0 / context->audioSampleRate;
    
    	return true;
    }
    
    void render(BelaContext *context, void *userData)
    {
    	for(unsigned int n = 0; n < context->audioFrames; n++) 
    	{
    		for(unsigned int c = 0; c < gOscs.size() && c < context->analogInChannels; ++c)
    		{
    			// generate a sine wave with the amplitude and frequency
    			gLFOs[c] = gAmplitude * gOscs[c].process(gFrequency * powf(2, c)) + DCamplitude;
    			gIns[c] = analogRead(context, n / gAudioFramesPerAnalogFrame, c);
    			analogWriteOnce(context, n / gAudioFramesPerAnalogFrame, c, gLFOs[c]);
    		}
    		// log the sine wave and sensor values on the scope
    		scope.log(gIns[0], gLFOs[7], gIns[1]);
    	}
    }
    
    void cleanup(BelaContext *context, void *userData)
    {
    }

    Now that it's refactored, I see that you are outputting sinewaves with an amplitude of 4.096 and an offset of 2.048. This means you are calling analogWriteOnce() with values between 0 and 4.096. However, outputs passed to analogWrite{once}() and inputs read by analogRead() are normalised to the 0:1 range. So any value you send out above 1 will be clipped to 1. This explains why you are seeing a clipped waveform when you read it back.

    Btw, the 0:1 range is mapped to 0V to 5V in the DAC while the input 0:1 range corresponds to 0V:4.096V in the ADC.

    See more details here.

    eluke309 Edit: Furthermore, without anything connected to them, the maximum DC voltage at my inputs is about 0.9V, give or take.

    Again, this is not actually Volts you are reading, but a normalised value between 0 and 1.
    Pepper further scales down the CV ins from a nominal range of 0V:10V to the 0V:4.096V range of the ADC. This is done via a resistor divider with a nominal ratio of 100k/(150k+100k) = 0.4, so a 10V input should translate to 4V at the ADC input, which is 4V/4.096V = 0.97 (this is the value you should read with analogRead()). This is at the net of components tolerances, which may vary widely. The 100k element in the resistor divider is the potentiometer, which can easily have ±10% tolerance.

    Thanks a lot for taking the time to reply and make the code so much simpler, I was unaware of many of these conversions and the oscillator library

    I am really grateful for the amount of support given on the forum and for the platform as a whole, truly makes a difference for the noobs that are just starting out : ).

    Cheers