0

EDIT 2 + Answer

Turns out I needed to wrap between 0 and Frequency*2*Math.pi.

Everyone who posted contributed to figuring out this issue. Since guest had the lowest reputation, I just marked his post as the answer. Thanks so much everyone, this was driving me crazy!

EDIT 1

Here's my WrapValue method, should have thought to post this before. It's not as sophisticated as Chris Taylor's, but it's having the same effect on my end.

public static double WrapValue(double value, double min, double max)
{
    if (value > max)
        return (value - max) + min;
    if (value < min)
        return max - (min - value);
    return value;
}

This might be appropriate for Gamedev, but it's less game-y and more code-and-math-y so I put it here.

I'm trying to turn my Xbox into a digital instrument using the new XNA 4.0 DynamicSoundEffectInstance class, and I'm getting a click every second. I've determined this is caused by any attempt to wrap my domain value between 0 and 2*pi..

I wrote a little class called SineGenerator that just maintains a DynamicSoundEffectInstance and feeds it sample buffers generated with Math.Sin().

Since I want to be precise and use the 44.1 or 48k sampling rate, I'm keeping a double x (the angle I'm feeding Math.Sin()) and a double step where step is 2 * Math.PI / SAMPLING_FREQUENCY. Every time I generate data for DynamicSoundEffectInstance.SubmitBuffer() I increment x by step and add sin(frequency * x) to my sample buffer (truncated to a short since XNA only supports 16 bit sample depth).

I figure I'd better wrap the angle between 0 and 2*pi so I don't loose precision for x as it gets large. However, doing this introduces the click. I wrote my own double WrapValue(double val, double min, double max) method in case MathHelper.WrapAngle() was being screwy. Neither wrapping between Math.PI and -Math.PI nor 0 and 2*Math.PI will get rid of the clicking. However, if I don't bother to wrap the value and just let it grow, the clicking disappears.

I'm thinking it has something to do with the accuracy of the .NET trig functions, how sin(0) != sin(2*pi), but I don't know enough to judge.

My question: Why is this happening, and should I even bother wrapping the angle?

The code:

using System;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework;

namespace TestDynAudio
{
    class SineGenerator
    {
  // Sample rate and sample depth variables
  private const int BUFFER_SAMPLE_CAPACITY = 1024;
  private const int BIT_RATE = 16;
  private const int SAMPLING_FREQUENCY = 48000;
  private readonly int BYTES_PER_SAMPLE;
  private readonly int SAMPLE_BUFFER_SIZE;

        private DynamicSoundEffectInstance dynSound = new DynamicSoundEffectInstance(SAMPLING_FREQUENCY, AudioChannels.Mono);
  private double x = 0; // The domain or angle value
  private double step;  // 48k / 2pi, increment x by this for each sample generated
  private byte[] sampleData; // The sample buffer
  private double volume = 1.0f; // Volume scale value

  // Property for volume
  public double Volume
  {
   get { return volume; }
   set { if (value <= 1.0 && value >= 0.0) volume = value; }
  }

  // Property for frequency
  public double Frequency { get; set; }

        public SineGenerator()
        {
   Frequency = 440; // Default pitch set to A above middle C
   step = Math.PI * 2 / SAMPLING_FREQUENCY;
   BYTES_PER_SAMPLE = BIT_RATE / 8;
   SAMPLE_BUFFER_SIZE = BUFFER_SAMPLE_CAPACITY * BYTES_PER_SAMPLE;
   sampleData = new byte[SAMPLE_BUFFER_SIZE];

   // Use the pull-method, DynamicSoundEffectInstance will
   // raise an event when more samples are needed
   dynSound.BufferNeeded += GenerateAudioData;
        }

  private void buildSampleData()
  {
   // Generate a sample with sin(frequency * domain),
   //   Convert the sample from a double to a short,
   //   Then write the bytes to the sample buffer
   for (int i = 0; i < BUFFER_SAMPLE_CAPACITY; i++)
   {
    BitConverter.GetBytes((short)((Math.Sin(Frequency * x) * (double)short.MaxValue) * volume)).CopyTo(sampleData, i * 2);

    // Simple value wrapper method that takes into account the
    // different between the min/max and the passed value
    x = MichaelMath.WrapValue(x + step, 0, 2f * (Single)Math.PI);
   }
  }

  // Delegate for DynamicSoundInstance, generates samples then submits them
  public void GenerateAudioData(Object sender, EventArgs args)
  {
   buildSampleData();
   dynSound.SubmitBuffer(sampleData);
  }

  // Preloads a 3 sample buffers then plays the DynamicSoundInstance
  public void play()
  {
   for (int i = 0; i < 3; i++)
   {
    buildSampleData();
    dynSound.SubmitBuffer(sampleData);
   }
   dynSound.Play();
  }

  public void stop()
  {
   dynSound.Stop();
  }
    }
}
michael.bartnett
  • 787
  • 7
  • 20
  • You say it goes away if you let it grow; does that mean there's no clicking at all if you start at 0 and go out from there, or that it disappears at some point? Have you tried other 2*pi intervals, like 1*pi - 3*pi, or 2C*pi, like 100*pi to 200*pi? – Matt Mills Dec 30 '10 at 21:10
  • I tried the multiples of 2*pi, but unfortunately it just increases the time between clicks. Thanks for your suggestion though. – michael.bartnett Jan 05 '11 at 20:18

3 Answers3

1

You did not show your wrap function, I did a quick test the following did not give any audible clicks.

My quick wrap function

public static float Wrap(float value, float lower, float upper)
{
  float distance = upper - lower;
  float times = (float)System.Math.Floor((value - lower) / distance);

  return value - (times * distance);
}

Called like this

x = Wrap((float)(x + step), 0, 2 * (float)Math.PI);
Chris Taylor
  • 52,623
  • 10
  • 78
  • 89
  • Thanks for pointing out that I left out the wrap method, added it at the top. No clicks at all when you use your wrap method? I replaced mine with yours, exact same call, but I'm still getting the click every second. – michael.bartnett Dec 29 '10 at 13:37
  • @bearcdp, are you experiencing this on the XBOX or the PC? I do not have an XBOX and only play with XNA on the PC, so this might be the cause. Your wrap function looks fine and just to be certain I tested on the PC with your wrap function and it seemed to work as well. I have no XBox dev experience, but could this simply be performance related? Have you tried doing the extra work of wrapping the value but ignore the result, just let `x` increment, maybe the problem is a slight increase in the time to build the buffer which presents as a click. – Chris Taylor Dec 29 '10 at 13:50
  • Thanks for your reply, I appreciate you taking the time to help me out. I'm just running on PC for now. Interestingly enough, allowing `x`to increment eliminates the problem. I also covered the possibility of the buffer taking too long to build by expanding the buffer size to 8192 samples, but I'm still getting clicks. – michael.bartnett Dec 29 '10 at 20:19
1

Could precision errors be caused by converting doubles to floats for your wrap function? What is the purpose of this anyway since you are originally using doubles and getting a double back. You are doing 48k conversions per second after all and the errors would build up. Your wrap function wouldn't work also if xstep is ever more than 2PI, but I don't see how that could happen...

If you stop casting as floats and that doesn't fix your problem, I recommend you create an x2 variable and set a breakpoint to see why the values are different:

// declarations
private double x2 = 0;
private byte[] sampleData2 = new byte[BUFFER_SAMPLE_CAPACITY * BYTES_PER_SAMPLE];

// replace your for loop with this and set a breakpoint on
// the line with Console.WriteLine()
for (int i = 0; i < BUFFER_SAMPLE_CAPACITY; i++)
{
    BitConverter.GetBytes((short)((Math.Sin(Frequency * x) * (double)short.MaxValue) * volume)).CopyTo(sampleData, i * 2);
    BitConverter.GetBytes((short)((Math.Sin(Frequency * x2) * (double)short.MaxValue) * volume)).CopyTo(sampleData2, i * 2);

    if (sampleData[i * 2] != sampleData2[i * 2] || sampleData[i * 2 + 1] != sampleData2[i * 2 + 1]) {
        Console.WriteLine("DIFFERENT VALUES!");
    }

    // Simple value wrapper method that takes into account the
    // different between the min/max and the passed value
    x = MichaelMath.WrapValue(x + step, 0, 2f * (Single)Math.PI);
    x2 = x2 + step;
}
Jason Goemaat
  • 28,692
  • 15
  • 86
  • 113
  • I didn't follow this exact code, but I did write a log of all the x values wrapped, the raw sin(x), and sin(frequency * x), and it revealed that at the 2*pi to 0 crossing, although the transition was smooth for the raw sin, multiplying it by Frequency caused a jump of 0.8, ouch. I should have thought to do some plain old visual examination before, thanks for your answer! – michael.bartnett Jan 05 '11 at 20:23
1

I am guessing that your Wrap function works fine, but you are not taking Sine(x) you are taking Sine(Frequency * x) - if Frequency*x doesn't not produce a round multiple of 2*PI then you get the pop. Try wrapping Frequency*x to get rid of the pop.

Guest
  • 26
  • 1
  • Wrapping Frequency * x doesn't work, but after looking at the log file I made after the answer Jason Goemaat posted, turns out the upper bound of the wrap function needs to be Frequency*2*pi. – michael.bartnett Jan 05 '11 at 20:31