Sine Wave Help?

Found a quick little loop to draw a sine wave on the screen, over here https://www.youtube.com/watch?v=jbNjSENyFTY and then added in an offset to animate it… but it is slow, and almost certainly not the best way to do it?

Problem is I must not have paid enough attention in mathematics at school, and am hoping someone else here did? :blush: (second problem would be, being able to dynamically adjust the frequency / width of the wave, and have the two ‘ends’ meet up?)

#include <Arduboy2.h>

Arduboy2 arduboy;

uint8_t offset = 0;

void setup()
{
  arduboy.boot();
}

void loop()
{
  if(offset == 125) offset = 0;
  
  for(float xval = 0; xval < 125; xval++)
  {
    double yval = -15*sin(xval/5)+32;
    uint8_t x = (uint8_t)xval + offset;
    if(x > 125) x=x-125;
    uint8_t y = (uint8_t)yval; 
    arduboy.drawPixel(x, y, WHITE);
  }
  
  arduboy.display(CLEAR_BUFFER);

  offset++;
}
1 Like

When you say “it’s slow”, did you actually test how fast/slow it is?

If you’re willing to sacrifice accuracy, fixed points might be faster.

I’m a bit confused about the xval / 5 part and the choice of 125.
The input to sin is supposed to be in radians where Pi radians = 180 degrees (or, if you’re a Tau supremecist like me, Tau = 360 degrees).

xval / 5 is going to net you a range from 0 to 24.8 in increments of 0.2.
You could a void the (usually expensive) division here by using addition (e.g. for(float xval = 0; xval < 125; xval += 0.2)), but there would be rounding errors.

I’m guessing 32 is HEIGHT / 2 and -15 is just 32 / 2 - 1 (with the negation to invert the y to account for y = 0 being the top instead of the bottom).


I tweaked the code slightly to improve the semantics a bit:

#include <Arduboy2.h>

Arduboy2 arduboy;

uint8_t offset = 0;

void setup()
{
	arduboy.boot();
}

constexpr uint8_t steps = 125;
constexpr uint8_t offsetMin = 0;
constexpr uint8_t offsetMax = steps - 1;

constexpr uint8_t halfWidth = WIDTH / 2;
constexpr uint8_t halfHeight = HEIGHT / 2;

void loop()
{
	
	for(uint8_t xval = 0; xval < steps; xval++)
	{
		double yval = halfHeight + (-15.0 * sin(xval / 5.0f));
		uint8_t y = static_cast<uint8_t>(yval); 
		uint8_t x = static_cast<uint8_t>(xval) + offset;
		
		if(x > steps)
			x -= steps;
			
		arduboy.drawPixel(x, y, WHITE);
	}

	if(offset < offsetMax)
		++offset;
	else
		offset = offsetMin;
	
	arduboy.display(CLEAR_BUFFER);
}
1 Like

The 125 value was used as an xOffset. Was the original screen a different dimension to the Arduboy? Should 125 be 128?

125 was to make the two ‘ends’ of the wave line up - there is probably a better mathematical way to be sure that the start and the end of the wave are the same point?

Why is having the start and end being at the same point desirable?
Just aesthetic reasons?

I’m not sure it would be possible for all frequencies.

I’m also slightly confused about the xval / 5 thing.

Indeed it would be very difficult to give the beginning and the end with a change of frequency with this code! We should review another technique!

#include <Arduboy2.h>

Arduboy2 arduboy;

uint8_t offset = 0;
uint8_t Vdef = 20;
uint8_t mx=0,my=0;
uint8_t Step=10;
uint8_t Tune=0;
void setup()
{
  arduboy.boot();
}

constexpr uint8_t steps = 125;
constexpr uint8_t offsetMin = 0;
constexpr uint8_t offsetMax = steps - 1;

constexpr uint8_t halfWidth = WIDTH / 2;
constexpr uint8_t halfHeight = HEIGHT / 2;

void loop()
{
  if (arduboy.pressed(LEFT_BUTTON)) {Step >(1)?Step--:1;}
if (arduboy.pressed(RIGHT_BUTTON)) {Step <(125)?Step++:125;}
  if (arduboy.pressed(DOWN_BUTTON)) {Vdef >(1)?Vdef--:1;}
if (arduboy.pressed(UP_BUTTON)) {Vdef <(30)?Vdef++:38;}
  for(uint8_t xval = 0; xval < steps; xval++)
  {
    double yval = halfHeight + (-Vdef * sin(xval / (.5f*Step)));
   
    uint8_t y = static_cast<uint8_t>(yval); 
    uint8_t x = static_cast<uint8_t>(xval) + offset;
    
  if(x > steps) x -= steps;
  x<mx?mx=x:mx;
  arduboy.drawLine(mx,my, x,y,WHITE);
  mx=x;my=y;
  }

  if(offset < offsetMax) ++offset;
  else
    offset = offsetMin;
  
  arduboy.display(CLEAR_BUFFER);
}

This won’t work for two reasons.

Firstly, you’d need to do Step = /*ternary expression*/ and Vdef = /*ternary expression*/ to assign.

(Likewise later on you have to do x = x < mx ? x : mx, but even then a ternary is a bit pointless because you end up assigning x to itself.)

Secondly, the way the ternary is evaluated would mean that Step = Step > 1 ? Step-- : 1; would be interpreted as:

if(Step > 1)
{
	const auto temp = Step;
	--Step;
	Step = temp;
}
else
{
	Step = 1
}

For cases like this, I prefer the following pattern:

if (arduboy.pressed(LEFT_BUTTON))
	if(Step > 1)
		--Step;

Easier to read and it uses less progmem because it compiles to fewer instructions.


Using drawline is the right idea for getting a smoother line.
It would be slower however.

I was inspired by what you said and I redid some of the code and it works perfectly! :wink:

#include <Arduboy2.h>
Arduboy2 arduboy;

uint8_t Vdef = 20;
uint8_t mx=0,my=0;
float add=5.5;
float degree=0;
float const degre=((3.1416*2)/360);

void setup()
{
  arduboy.boot();
}

constexpr uint8_t halfWidth = WIDTH / 2;
constexpr uint8_t halfHeight = HEIGHT / 2;
void loop()
{
  if (arduboy.pressed(LEFT_BUTTON)) {if (add > 0.2) {add=add-.1;}}
if (arduboy.pressed(RIGHT_BUTTON))  {if (add <20) {add=add+.1;}}
  if (arduboy.pressed(DOWN_BUTTON)) {if (Vdef > 1) {Vdef--;}}
if (arduboy.pressed(UP_BUTTON))  {if (Vdef <30) {Vdef++;}}
  for(uint8_t xval = 0; xval<128; xval++)
  {
  double yval = halfHeight + (-Vdef * sin((degree*degre)));
  if (xval<mx) {mx=xval;my=yval;}
 // arduboy.drawPixel(xval,yval, WHITE);
  arduboy.drawLine(mx,my,xval,yval,WHITE);
  mx=xval;my=yval;
  degree=degree+add;
 if( degree>=360) { degree=360-degree+add;}
}
arduboy.display(CLEAR_BUFFER);
}
1 Like

And based on that I cleaned it up, changed some of the functionality, renamed some stuff and documented it:

#include <Arduboy2.h>

constexpr float Tau = 6.28318530718;

/*float degreesToRadians(float degrees)
{
	constexpr float degreeToRadianFactor = (Tau / 360.0f);
	return degrees * degreeToRadianFactor;
}*/

constexpr float mapRange(float input, float inputMin, float inputMax, float outputMin, float outputMax)
{
    return outputMin + (input - inputMin) * ((outputMax - outputMin) / (inputMax - inputMin));
}

constexpr uint8_t screenWidth = WIDTH;
constexpr uint8_t screenHeight = HEIGHT;
constexpr uint8_t halfScreenWidth = WIDTH / 2;
constexpr uint8_t halfScreenHeight = HEIGHT / 2;

Arduboy2 arduboy;

// amplitude is the sine wave's
// distance from the vertical baseline in pixels.
// i.e. the height of the wave is 2 * amplitude
uint8_t amplitude = 20;

// frequency is the number of pixels a full wave spans
// on the horizontal axis.
uint8_t frequency = 16;

// sampleOffset is used to animate the sine wave
uint8_t sampleOffset = 0;

void setup()
{
	arduboy.boot();
}

// previousX and previousY are used for drawing
// the lines that make up the sine waves.
// They represent the endpoint of the previous line.
uint8_t previousX = 0;
uint8_t previousY = halfScreenHeight;

void loop()
{
	arduboy.pollButtons();
	
	if (arduboy.justPressed(LEFT_BUTTON))
		if (frequency > 2)
			frequency -= 2;
			
	if (arduboy.justPressed(RIGHT_BUTTON))
		if (frequency < 128)
			frequency += 2;
			
	if (arduboy.justPressed(DOWN_BUTTON))
		if (amplitude > 1)
			--amplitude;
	
	if (arduboy.justPressed(UP_BUTTON))
		if (amplitude < 30)
			++amplitude;
	
	// x represents the x coordinate of the current screen pixel
	// This is used as an offset for sampling sine
	for(uint8_t x = 0; x < screenWidth; ++x)
	{
		// theta is derived by mapping the sampleOffset
		// from a (0, frequency] range to a (0, Tau] range.
		float theta = mapRange((sampleOffset + x) % frequency, 0, frequency, 0, Tau);
		
		double y = halfScreenHeight + (-amplitude * sin(theta));
		
		if (x >= previousX)
			arduboy.drawLine(previousX, previousY, x, y, WHITE);
		
		previousX = x;
		previousY = y;
	}
	
	++sampleOffset;
	sampleOffset %= frequency;
	
	arduboy.display(CLEAR_BUFFER);
}

It almost certainly won’t be the fastest approach, but it’s probably the cleanest and/or closest to the mathematical way of looking at it.


For the record, if your keyboard doesn’t have a grave key (’`’) for writing markdown code blocks you can always use old style BB-code [code] [/code].

2 Likes

Clean and efficient!

1 Like

Many thanks @Pharap and @Electro-l.i.b! That is exactly what I was attempting… am using it with the watchX’s accelerometer for a kind of ‘faux-theremin’! :upside_down_face:

Here is the code (to use this with an Arduboy + accelerometer you’d just need to change TCCR1A / TCCR1B / OCR1A / COM1A0 to be TCCR3A / TCCR3B / OCR3A / COM3A0 and also check the I²C address is correct):

#include <Wire.h>
#include "Arduboy2.h"

uint8_t AcXH, AcXL;

constexpr float Tau = 6.28318530718;

constexpr float mapRange(float input, float inputMin, float inputMax, float outputMin, float outputMax)
{
  return outputMin + (input - inputMin) * ((outputMax - outputMin) / (inputMax - inputMin));
}

constexpr uint8_t screenWidth = WIDTH;
constexpr uint8_t screenHeight = HEIGHT;
constexpr uint8_t halfScreenWidth = WIDTH / 2;
constexpr uint8_t halfScreenHeight = HEIGHT / 2;

Arduboy2 arduboy;

// amplitude is the sine wave's
// distance from the vertical baseline in pixels.
// i.e. the height of the wave is 2 * amplitude
uint8_t amplitude = 20;

// frequency is the number of pixels a full wave spans
// on the horizontal axis.
uint8_t frequency = 16;

// sampleOffset is used to animate the sine wave
uint8_t sampleOffset = 0;

void setup()
{
  arduboy.boot();

  TCCR1B = (bit(WGM32) | bit(CS31)); // CTC mode. Divide by 8 clock prescale

  // Initialize MPU-6050
  Wire.begin();
  Wire.beginTransmission(0x69);
  Wire.write(0x6B);  // PWR_MGMT_1 register
  Wire.write(0);     // set to zero (wakes up the MPU-6050)
  Wire.endTransmission(true);
}

// previousX and previousY are used for drawing
// the lines that make up the sine waves.
// They represent the endpoint of the previous line.
uint8_t previousX = 0;
uint8_t previousY = halfScreenHeight;

void loop()
{
  Wire.beginTransmission(0x69); // I2C address of the MPU-6050
  Wire.write(0x3B); // starting with register 0x3B (ACCEL_XOUT_H)
  Wire.endTransmission(true);
  Wire.requestFrom(0x69,2,true);
  AcXH = Wire.read(); // 0x3B (ACCEL_XOUT_H)
  AcXL = Wire.read(); // 0x3C (ACCEL_XOUT_L)

  OCR1A = AcXH<<8|AcXL; // load the count (16 bits), which determines the frequency
  frequency = (127 & AcXH);
  
  arduboy.pollButtons();

  if (arduboy.justPressed(UP_BUTTON))
    TCCR1A = bit(COM1A0); // set toggle on compare mode (which connects the pin)  
        
  if (arduboy.justPressed(DOWN_BUTTON))
    TCCR1A = 0; // set normal mode (which disconnects the pin)

  // x represents the x coordinate of the current screen pixel
  // This is used as an offset for sampling sine
  for(uint8_t x = 0; x < screenWidth; ++x)
  {
    // theta is derived by mapping the sampleOffset
    // from a (0, frequency) range to a (0, Tau) range.
    float theta = mapRange((sampleOffset + x) % frequency, 0, frequency, 0, Tau);
    
    double y = halfScreenHeight + (-amplitude * sin(theta));
    
    if (x >= previousX)
      arduboy.drawLine(previousX, previousY, x, y, WHITE);
    
    previousX = x;
    previousY = y;
  }
  
  ++sampleOffset;
  sampleOffset %= frequency;
  
  arduboy.display(CLEAR_BUFFER);
}
2 Likes

The rule still stands! The more brain there is, the more we evolve rapidly! :wink:

2 Likes

Neat.

Now I want to stick some 9DoFs in a pair of gloves to create an invisible theramin.
Right hand for frequency and left hand for amplitude, or the other way around? :P


If you ever decide it’s still not fast enough, don’t forget fixed points are always an option.
(I do actually have a sine function, but it uses brads instead of radians, i.e. 256 brads = 360 degrees. Hopefully the reason I chose brads is obvious :P)


Or to choose a more traditional idiom:
“Two heads are better than one”

3 Likes

I would have said that too but I said something else out of politeness! we were three! Hahaha …

1 Like