Pergunta

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();
  }
    }
}
Foi útil?

Solução

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.

Outras dicas

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);

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;
}
Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top