The formula commonly used to represent a sinusoidal signal is
V = A cos(ωt)
where A is the amplitude and ω is the angular frequency. This formula
should cope well with a variable amplitude. It is, however, not suitable
for a signal with variable frequency, because a discontinuity of the
frequency is not supposed to create a discontinuity in the signal.
The correct formula for a signal where both the amplitude and the
frequency can vary is
V = A(t) cos(φ(t))
where the phase is computed as
φ(t) = ∫ ω(t)dt
In terms of code, this means you need a static variable to keep track of
the current phase, and you have to update it periodically in order to
perform the numerical integration.
Here is a tentative implementation. I have qualified some variables as
extern only to hint that you are responsible for managing them. You
can remove the qualifier if you integrate this into your own code:
// Defined and updated elsewhere.
extern const uint8_t output_channel;
extern float angular_frequency; // in rad/us
extern float amplitude, offset; // in ADC steps
// Call this as often as practical.
void update_output()
{
// Compute the time delta.
static uint32_t time_of_last_update;
uint32_t now = micros();
uint32_t dt = now - time_of_last_update;
time_of_last_update = now;
// Update the phase.
static float phase;
phase += angular_frequency * dt;
while (phase >= 2 * M_PI) phase -= 2 * M_PI; // unwrap
// Output the signal.
float output = offset + amplitude * cos(phase);
analogWrite(output_channel, round(output));
}
Regarding the second part of the question (about arbitrary waveshapes),
it is, of course, feasible. You just have to pass arbitrary values to
analogWrite().