Sine Wave Phase Issue

Status
Not open for further replies.
Hello,

I'm a little confused on how sine.phase() works.

I am trying to generate two sine waves, one for the left channel, and one for the right.

The sine waves have identical frequencies, but the right channel needs to variably be 90 or 270 degrees out of phase.

I am able to create the sine waves, and in setup() I can set sine.phase(90) on the right channel and get the correct output.

But when I try to change the phase in the loop() I run into issues:

In order to get to a 270 degree offset between the two do I need to use:
sine.phase(180)
or
sine.phase(270)

and to go back to a 90 degree offset, do I use:
sine.phase(180)
or
sine.phase(90)

I believe that the phase is persistent on the sine object, since it continues to be out of phase if I don't attempt to change it. So I think I should be able to just move it 180 degrees each time. But the documentation says that sine.phase() "Jumps to a specific point within it's cycle" which makes me think I need to use 90/270 as my values.

Either way my experimentation has not been fruitful. I also noticed a fair amount of popping when the phase changes, which I'm not sure how to avoid when making such a dramatic phase shift.

Am I missing something obvious here? Am I going about this the wrong way?

I did find this on stackoverflow, but I'm not sure if its possible/necessary to implement using the existing sine functions:
https://stackoverflow.com/questions...e-pops-from-concatented-sound-data-in-pyaudio
StackOverflow.com said:
A better method of stitching the waveforms together is to keep track of the phase from one tone and use that as the starting phase for the next.


The code below waits for a serial value like "a1000;" or "a-1000;" then changes the direction/phase based on the value being positive/negative and the frequency based on the absolute value of the integer. It works perfectly in one direction, but not the other.

Code:
#include <Audio.h>
#include <Wire.h>
#include <SD.h>
#include <SPI.h>
#include <SerialFlash.h>

#include <Audio.h>
#include <Wire.h>
#include <SPI.h>
#include <SD.h>
#include <SerialFlash.h>

// GUItool: begin automatically generated code
AudioSynthWaveformSine   sine1;          //xy=718,208
AudioSynthWaveformSine   sine2;          //xy=718,208
AudioOutputUSB           usb1;           //xy=1313,236
AudioOutputI2S           i2s2;           //xy=1318,168
AudioConnection          patchCord1(sine1, 0, i2s2, 0);
AudioConnection          patchCord2(sine2, 0, i2s2, 1);
AudioConnection          patchCord3(sine1, 0, usb1, 0);
AudioConnection          patchCord4(sine2, 0, usb1, 1);
AudioControlSGTL5000     sgtl5000_1;     //xy=743,433
// GUItool: end automatically generated code
int currentDirection;
int newDirection;


void setup() {
  
  AudioMemory(18);
  Serial.begin(115200);
  Serial1.begin(115200);
  //while (!Serial) ; // wait for Arduino Serial Monitor
  delay(200);
  sine1.frequency(0);
  sine2.frequency(0);
  sgtl5000_1.enable();
  sgtl5000_1.volume(0.45);
  sine2.phase(90);
  currentDirection=0;
  newDirection=0;

}
char buffer[16];
void loop() {
   int incomingByte;
   
   // If no serial keep doing what its doing
   while(!Serial1.available());
   int startChar = Serial1.read();
   

   if (startChar == 'a'){
     int size = Serial1.readBytesUntil(';', buffer, 16);
     String myString = String(buffer);
     Serial.println("timecode input detected");
     
     int value = myString.toInt();
     if (value > 100 || value < -100){
      Serial.println("value");
      Serial.println(value);
      if (value < 0) {
         newDirection = 1;
         if (currentDirection != newDirection ){
          sine2.phase(180);
         }
         currentDirection = newDirection;
         usbMIDI.sendControlChange(48, 1, 2);
         sine1.frequency(-value);
         sine2.frequency(-value);
      } else if (value > 0) {
         newDirection = 0;
         if (currentDirection != newDirection ){
            sine2.phase(180);
         }
         currentDirection = newDirection;
         usbMIDI.sendControlChange(48, 127, 2);
         sine1.frequency(value);
         sine2.frequency(value);
      }
      currentDirection = newDirection;
      delay(1);
     } else {
       sine1.frequency(0);
       sine2.frequency(0);
     }
     

    
   } else if (startChar == 'b'){
    Serial.println("device type 2 input detected");
    int size = Serial1.readBytesUntil(';', buffer, 16);
    String myString = String(buffer);
    Serial.println(myString);
    
   } else if (startChar == 'c'){
    Serial.println("device type 3 input detected");
    int size = Serial1.readBytesUntil(';', buffer, 16);
    String myString = String(buffer);
    Serial.println(myString);
    
   } else {
    Serial.println("nope");
    
   }

}

P.S. I got the startChar concept from the VideoDisplay.ino in the Octows2811 Library and I love it, but I'm just using abc as placeholders for now while I work out the phase stuff first.
 
Last edited:
To elaborate more on what I'm actually trying to accomplish...

I am working on a project that uses the Teensy 3.2, Audio Shield, and an esp-01s connected via serial. We'll call this the master device.

I have a 2nd ESP-01s with an i2c accelerometer sending UDP packets to the master with the data from the i2c input. We'll call this the slave device.

The master device generates a sine wave at a frequency based on the input data from the slave device, and outputs the sine wave through usb and i2s.

Everything is working, and that is cool, but I'm not done yet.

If you're not familiar with Digital Vinyl Systems (DVS), they essentially work by reading a tone, measuring the frequency, and then adjusting the playback of an audio file based on the frequency.

If you want a full in depth breakdown, this is a great article, but you don't need all of it for my issue:
https://djtechtools.com/2009/08/03/timecode-exlplained-basic/


A control tone Vinyl/CD might play back a 1khz tone when rotating at 33rpm. But will generate a 2khz tone when moving at 66rpm.
Thus
500hz = 50% playback speed
1khz = 100% playback speed
2khz = 200% playback speed
etc.

However that only get's us the playback speed of the track, but doesn't tell us anything about the direction.

All control tone tracks have both a left and right channel. By phase shifting the right channel 90 degrees, you can discern the direction of the playback by comparing the amplitude between the two channels.

djtechtools.com said:
Because of the phase shifting the right channel is a bit ahead of the left channel. When the right channel meets the zero line, then the values of the amplitude of the left channel are negative. When the playback direction is reversed and the right channel reaches the zero line, then the values of the amplitude of the left channel are positive.

linksrechts_shift.jpg


My problem is that I am generating the tones with the correct phase offset but I am not able to play it "backwards". Offsetting the phase on the right channel to 270 degrees instead of 90 should accomplish the same thing though.
 
So I figured out what was happening.

Essentially the phase of my sine wave was being jumped to a position but didn't take into account the current position of the wave.
Uk1aj4t.png

Unfortunately there didn't seem to be a way to get the current phase in degrees, so I had to modify synth_sine.h and make the phase_accumulator public.

Looking at synth_sine.cpp we can see that to get the index of the phase, we need to use index = phase_accumulator >> 24;

and that index refers to the sine wave object found in data_waveforms.c which contains 257 items.

So 360 / 257 ~= 1.4

Which ultimately means I can use sine.phase(index*1.4)

And it works! You can see the bottom sine wave make an "M" shape when it changes directions.
BzwkTGp.jpg

Now this is a pretty hacky workaround, I think it might be better to implement a get_phase_degree() method in the AudioSynthWaveformSine class.The math on the degree calculation could use a little tweaking to be more accurate. Maybe something like:
index = ph >> 24;
index += 266 / 2;
index %= 267;

But I'm still experimenting. Any thoughts or input would be appreciated!

Edit:
It appears to mostly work. The problem is that with the lack of precision on the degree conversion as is, the right channel gradually goes out of phase from where it should be when dealing with a lot of direction changes.
 
Last edited:
So I was able to get it working, as you can see from the video below.

I ended up using a mixer and reversing sine2 with a negative gain on the right channel when going backwards. However, I noticed that switching directions rapidly caused the two waves to get out of sync still. Same problem I had when just jumping the phase around.


So I ended up using the process I mentioned above to get the current phase index of sine1 I then took that index and converted it to degrees as above.
Then it was just a matter of using sine2.phase(degrees+270) to set the offset for the 2nd sine wave.

I only needed to do this in one direction. I simply adjust the mixer gain back on the other.

 
Try the following direct IQ generator Audio block, I think it does what you want. I've used it in my SDR work for a couple of years.
Bonus: It works on the Teensy 4.0 with the Audio card :D

Code:
//-------------------------------------------------------------------------------------
//                        *** AudioIQsine.ino ***
//
// Sample sketch to demonstrate Audio board quadrature sine generation
//
// Author:  Derek Rowell
// Updated: Sept 12, 2019
//  
//-------------------------------------------------------------------------------------
#include <Audio.h>
#include "AudioIQsine.h"
//
AudioOutputI2S   output;
AudioIQsine      sine;
AudioControlSGTL5000 codec;
//
AudioConnection c1(sine, 0, output, 0);
AudioConnection c2(sine, 1, output, 1);
//----------------------------------------------------------------------------
void setup() {
   codec.enable();
   AudioMemory(12);
   sine.amplitude(0.95);
}
//----------------------------------------------------------------------------------
void loop() {
  // Show 90 deg and 270 deg shifts by sign change
  // in frequency
  sine.frequency(1000.0);
  delay(1000);
  sine.frequency(-1000.0);   
  delay(1000);
  // Repeat with a different frequency
  sine.frequency(2000.0);
  delay(1000);
  sine.frequency(-2000.0);   
  delay(1000);
}
with:
Code:
//---------------------------------------------------------------------------------
// File:    AudioIQsine.h
//          A ''Teensy'' Audio Object function for quadrature sine wave
//          generation.
//
// Note:    As in the normal convention, the I function is the cosine term,
//          and the Q function is the sine term so thet
//           f_quad(t) = I(t) + jQ(t) = cos(2.pi.f.t) + jsin(2.pi.f.t)
//
// Author:  Derek Rowell
// Date:    Sept 12. 2019
//
// Copyright 2019, Derek Rowell
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
// of the Software, and to permit persons to whom the Software is furnished to do
// so, subject to the following conditions:
//  1) The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
// 2) THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 
// ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//---------------------------------------------------------------------------------
#ifndef audio_IQ_sine_h_
#define audio_IQ_sine_h_
#include "Arduino.h"
#include "AudioStream.h"

class AudioIQsine : public AudioStream {
  public:
    AudioIQsine() : AudioStream(0,NULL), _sinPhase(0.0), _cosPhase(16384.0){ }       // corresponds to phase shift of pi/2
    // ---
    virtual void update(void);
    // ---
    void frequency(float freq) {     // allow negative frequencies
      if (freq < 0.0) {              //  e^(jwt)  = cos(wt) + jsin(wt)
        _sinesign = -1;
        _freq     = -freq;           //  e^(-jwt) = cos(wt) - jsin(wt)
      } else {
        _sinesign = 1;
        _freq = freq;
      }
      if (_freq > AUDIO_SAMPLE_RATE_EXACT/2) _freq = AUDIO_SAMPLE_RATE_EXACT/2;
      _phaseInc = _freq*(65536.0/float(AUDIO_SAMPLE_RATE_EXACT));
    }
    // ---
    void amplitude(float amp) {
      if (amp <= 0.0)       _mag = 0;
      else if (amp >= 1.0)  _mag = 32767;
      else                  _mag = int(amp*32767.0);
    }
//
  private:
    int16_t  _sinesign  = 1;
    uint16_t _mag;
    float    _freq      = 0.0;
    float    _sinPhase;
    float    _cosPhase;
    float    _phaseInc  = 0.0;
  };
  #endif
and
Code:
//---------------------------------------------------------------------------------
// File:    AudioIQsine.cpp
//          A ''Teensy'' Audio Object function for quadrature sine wave
//          generation.
//
// Note:    As in the normal convention, the I function is the cosine term,
//          and the Q function is the sine term so thet
//           f_quad(t) = I(t) + jQ(t) = cos(2.pi.f.t) + jsin(2.pi.f.t)
//
// Author:  Derek Rowell
// Date:    Sept 12. 2019
//
// Copyright 2019, Derek Rowell
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
// of the Software, and to permit persons to whom the Software is furnished to do
// so, subject to the following conditions:
//  1) The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
// 2) THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 
// ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//---------------------------------------------------------------------------------
#include "AudioIQsine.h"
extern "C" {extern const int16_t AudioWaveformSine[257];}
void AudioIQsine::update(void) {
  audio_block_t *blockI, *blockQ;
  uint16_t index, intPhase;         // Index into sine table
  int16_t  delta;                   // Remainder for interpolation,
  int16_t  val1, val2;
  blockI = allocate();
  blockQ = allocate();
  if (!blockI && !blockQ) return;
  if ( blockI && !blockQ) {release(blockI); return;}
  if (!blockI &&  blockQ) {release(blockQ); return;}
  // Main loop:
  for (uint16_t i=0; i < AUDIO_BLOCK_SAMPLES; i++) {  
    // Quadrature oscillator outputs (sine and cosine) 
    // using an 8-bit look-up table with linear interpolation.
    intPhase = (uint16_t)_sinPhase;  
    index    = intPhase>>8;                        // index into lookup table
    delta    = intPhase&0xFF;                      // remainder 
    val1     = AudioWaveformSine[index];
    val2     = AudioWaveformSine[index+1];         // table has 257 entries
    blockQ->data[i] = _sinesign*(_mag*(val1 + (((val2 - val1)*delta)>>8)))>>15;  // linear interpolation

    intPhase = (uint16_t)_cosPhase;  
    index    = intPhase>>8;
    delta    = intPhase&0xFF;
    val1     = AudioWaveformSine[index];
    val2     = AudioWaveformSine[index+1];
    blockI->data[i] = (_mag*(val1 + (((val2 - val1)*delta)>>8)))>>15;

    _sinPhase += _phaseInc;
    if (_sinPhase>65536.0) _sinPhase = _sinPhase - 65536.0;
    _cosPhase += _phaseInc;
    if (_cosPhase>65536.0) _cosPhase = _cosPhase - 65536.0;
  };
  transmit(blockI, 0); // Use convention that the I channel (cosine) is on output 0,
  transmit(blockQ, 1);   
  release(blockI);
  release(blockQ);
  return;
}
 
Follow-on from above: If you want to take an arbitrary waveform, say an existing sinusoid, and alter its phase by 90 or 270 deg, I can let you have a "good" Hilbert transformer audio block. It takes a single (mono) input and produces the IQ pair. The only problem is that it introduces a 128 sample delay (~3 ms) to both channels.

Derek
 
Follow-on from above: If you want to take an arbitrary waveform, say an existing sinusoid, and alter its phase by 90 or 270 deg, I can let you have a "good" Hilbert transformer audio block. It takes a single (mono) input and produces the IQ pair. The only problem is that it introduces a 128 sample delay (~3 ms) to both channels.

Derek

Thanks for the tips. Responsiveness is #1 priority here, and it seems really fast with my solution, but I haven't measured the actual speed yet.

TBH really don't know a ton about signal processing. I looked up the Hilbert Transform as well as IQ, but I have no frame of reference for if these solutions ares better or worse performance-wise from what I am doing.

I don't even know what to lookup in terms of what I'm doing. Your solution uses a Hilbert Transform, my solution uses a ____?

This has been quite the learning experience for me. But I was able to make something that does (seemingly) what I wanted it to.
 
Thanks for the tips. Responsiveness is #1 priority here, and it seems really fast with my solution, but I haven't measured the actual speed yet.

TBH really don't know a ton about signal processing. I looked up the Hilbert Transform as well as IQ, but I have no frame of reference for if these solutions ares better or worse performance-wise from what I am doing.

I don't even know what to lookup in terms of what I'm doing. Your solution uses a Hilbert Transform, my solution uses a ____?

This has been quite the learning experience for me. But I was able to make something that does (seemingly) what I wanted it to.

there seems to be quite some latency in your processing still when comparing the audio and the video (but that might be youtube). In any case, I've deon exactly what you are trying to do, and the only thing you need to do is change one channels phase by 180 deg. No need to hack it together. It is true that it doesn't take into account the current position but that doesn't matter for the final outcome, and interpretation of the dvs software.

I didn't look at the code at length but at least make sure you use audio no interrupts and audiointerrups properly, and then just set the phase FOR BOTH channels. You'll only change the offset on one, but in order for the other channel to reset to the right relative phase v.s the other channel you need to set it as well.
 
Status
Not open for further replies.
Back
Top