Author Topic: BASS.Net - Unable to output "perfect" sine waves  (Read 9491 times)

AName123

  • Guest
Hello, I've been using this code to try to output a constant sine wave at a selected note's pitch based off of it's distance in half steps from A4. I've been successful in doing the math correctly, but due to the fact that I (obviously) can't make infinite calculations and sound outputs, I've had trouble getting it to "line up with itself" at the end and beginning of the sample (on some pitches, at least), causing it to sound raspy.

I've noticed that increasing the default sample rate (frequency) to 112640 made the problem smaller, but it didn't get rid of it... I've also tried my own buffering technique that would calculate more of the sine wave (until it ended up back at 0), but that didn't work. :P

I have a feeling that I'm not looking at this the right way. Can you fix my code or at least tell me how to fix this problem?

Code: [Select]
            Console.WriteLine("Enter the # of half steps DOWN from A4 (use - to go up):");
            int halfSteps = Convert.ToInt32(Console.ReadLine());
            double frequency = 1024d * Math.Pow(2d, ((double)halfSteps / 12d));
            int frequencyInt = (int)frequency;

            //--Set up sample--
            int sampleLength = frequencyInt;
            int sample = Bass.BASS_SampleCreate(sampleLength, 112640, 1, 1, BASSFlag.BASS_SAMPLE_LOOP | BASSFlag.BASS_SAMPLE_OVER_POS); //28160
            //--Sample ready--

            short[] sampleData = new short[sampleLength];

            double wavePosition = 0;
            short waveHeight = 0;

            for (int a = 0; a < frequencyInt; a++)
            {
                waveHeight = (short)(2000.0 * Math.Sin(wavePosition * 6.283185 / 64d)); //32767.0

                sampleData[a] = waveHeight;
                wavePosition += 128d / frequency;
                if (wavePosition > frequency)
                {
                    wavePosition = 0;
                }
            }

            //--Play sample--
            Bass.BASS_SampleSetData(sample, sampleData);
            int channel = Bass.BASS_SampleGetChannel(sample, false);
            Bass.BASS_ChannelPlay(channel, false);
            //--Sample playing--
           
            Console.ReadLine();
            Bass.BASS_SampleFree(sample);

AName

  • Posts: 20
Re: BASS.Net - Unable to output "perfect" sine waves
« Reply #1 on: 19 Mar '11 - 15:54 »
Ahh... No wonder it asked me for a name and email and I didn't have a "Notify me of replies" checkbox - I wasn't logged in. :O

Ian @ un4seen

  • Administrator
  • Posts: 26172
Re: BASS.Net - Unable to output "perfect" sine waves
« Reply #2 on: 21 Mar '11 - 16:14 »
The issue when using a sample to play a continuous tone will be having it contain a whole number of sine waves. A click will be heard when it loops otherwise.

It is simpler/better to use a stream instead, ie. BASS_StreamCreate instead of BASS_SampleCreate. You would then generate the tone in the stream's STREAMPROC callback function.

AName

  • Posts: 20
Re: BASS.Net - Unable to output "perfect" sine waves
« Reply #3 on: 22 Mar '11 - 01:55 »
I see.

I've written this code and it looks perfect to me, but WaveformProc is never called. Can you tell me why?

Code: [Select]
        public Form1()
        {
            InitializeComponent();
        }

        STREAMPROC streamCreate;

        int channel;

        byte[] data = null; //buffer
        double wavePosition = 0;
        double frequency;

        private void playSine()
        {
            frequency = 128d;

            streamCreate = new STREAMPROC(WaveformProc);

            channel = Bass.BASS_StreamCreate(44100, 2, BASSFlag.BASS_DEFAULT, streamCreate, IntPtr.Zero);
            Bass.BASS_ChannelPlay(channel, false);
        }

        private int WaveformProc(int handle, IntPtr buffer, int length, IntPtr user)
        {
            if (data == null || data.Length < length)
            {
                data = new byte[length];
            }

            int waveCalculated = length; //waveCalculated |= (int)BASSStreamProc.BASS_STREAMPROC_END;

            int waveHeight;

            for (int a = 0; a < length; a++)
            {
                waveHeight = (short)(2000.0 * Math.Sin(wavePosition * 6.283185 / 64d)); //32767.0

                data[a] = Convert.ToByte(waveHeight);

                wavePosition += 128d / frequency;
                if (wavePosition > frequency)
                {
                    wavePosition = 0;
                }
            }

            Marshal.Copy(data, 0, buffer, length);

            return waveCalculated;
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            playSine();
        }

Ian @ un4seen

  • Administrator
  • Posts: 26172
Re: BASS.Net - Unable to output "perfect" sine waves
« Reply #4 on: 22 Mar '11 - 13:08 »
Just to be sure, did you call BASS_Init? :) ... If so, please check the BASS function return values to see whether they are reporting success.

Also, you are creating a 16-bit stream in the BASS_StreamCreate call, so the "data" array should be of "short" type rather than "byte". The STREAMPROC (WaveformProc) appears to be generating a mono stream, in which case the BASS_StreamCreate "chans" parameter should be 1.

Note the STREAMPROC "length" parameter is in bytes, so that will also need to be converted to samples for your processing, eg. like this...

Code: [Select]
        private int WaveformProc(int handle, IntPtr buffer, int length, IntPtr user)
        {
            length /= 2; // convert bytes to samples (16-bit mono = 2 bytes)
...
            return length * 2;
        }

AName

  • Posts: 20
Re: BASS.Net - Unable to output "perfect" sine waves
« Reply #5 on: 22 Mar '11 - 23:18 »
Ehh... Excuse my noobiness (I did in fact forget to call Bass.BASS_Init - I was copying code from the console program that I wrote my very first post's code in).  :D

I think the raspy sound was actually coming from a mistake in my coding. Previously I set wavePosition to 0 - this is the correct way to do it:
Code: [Select]
                wavePosition += increment;
                if (wavePosition > twoPI)
                {
                    wavePosition -= twoPI;
                }

Anyways, I got it working! Here is the code that will play 2 sine waves (based off of any note's distance from A4) - the second is a major third above the first:

Code: [Select]
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using Un4seen.Bass;
using System.Runtime.InteropServices;

namespace Custom_Studio
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        STREAMPROC streamCreate;

        const double twoPI = 6.283185;
        int sampleRate = 44100;

        int channel;

        short[] data = null; //buffer
        double wavePosition = 0;
        double frequency;
        double increment;
        double frequency2;
        double increment2;
        double wavePosition2 = 0;

        private void playSine()
        {
            //# of half steps UP from A4 (use - to go down)
            int halfSteps = 0;

            frequency = 220d * Math.Pow(2d, ((double)halfSteps / 12d));
            increment = twoPI * frequency / (double)sampleRate;

            frequency2 = 220d * Math.Pow(2d, (((double)halfSteps + 4) / 12d));
            increment2 = twoPI * frequency2 / (double)sampleRate;

            streamCreate = new STREAMPROC(WaveformProc);

            channel = Bass.BASS_StreamCreate(sampleRate, 2, BASSFlag.BASS_DEFAULT, streamCreate, IntPtr.Zero);
            Bass.BASS_ChannelPlay(channel, false);
        }

        private int WaveformProc(int handle, IntPtr buffer, int length, IntPtr user)
        {
            length /= 2;

            if (data == null || data.Length < length)
            {
                data = new short[length];
            }

            int waveCalculated = length * 2;
            //waveCalculated |= (int)BASSStreamProc.BASS_STREAMPROC_END;

            for (int a = 0; a < length; a++)
            {
                data[a] = (short)((2000.0 * Math.Sin(wavePosition)) + (2000.0 * Math.Sin(wavePosition2))); //32767.0

                if (data[a] > 5000)
                {
                    data[a] = 5000;
                }

                wavePosition += increment;
                if (wavePosition > twoPI)
                {
                    wavePosition -= twoPI;
                }

                wavePosition2 += increment2;
                if (wavePosition2 > twoPI)
                {
                    wavePosition2 -= twoPI;
                }
            }

            Marshal.Copy(data, 0, buffer, length);

            return waveCalculated;
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            if (Bass.BASS_Init(-1, sampleRate, BASSInit.BASS_DEVICE_DEFAULT, IntPtr.Zero))
            {
                playSine();
            }
        }
    }
}

I have three more questions...:

1. Couldn't I just set my data array to the maximum size it will ever need to be? I'm looking for ways to make this as fast as possible, and getting rid of this block of code seems to be the best way I can do that:

Code: [Select]
            if (data == null || data.Length < length)
            {
                data = new short[length];
            }

2. I kept it stereo and used 220 to calculate the frequency (instead of 440) because later on I will need to deal with panning and such. Am I using stereo correctly? I'm not quite sure about how to pan anything with the way I coded it - I do NOT want to use channel panning, btw.

3. Can you give me any insight as to whether or not you think this is a good idea: I'm considering generating one period of a sine wave in very tiny increments and storing it in an array when the program starts - then I can sacrifice memory for speed by not having to perform any "live" sine calculations. My concern is about slight inaccuracies that could potentially result from doing this.

Tyvm!

Ian @ un4seen

  • Administrator
  • Posts: 26172
Re: BASS.Net - Unable to output "perfect" sine waves
« Reply #6 on: 23 Mar '11 - 14:44 »
1. Couldn't I just set my data array to the maximum size it will ever need to be? I'm looking for ways to make this as fast as possible, and getting rid of this block of code seems to be the best way I can do that:

Code: [Select]
            if (data == null || data.Length < length)
            {
                data = new short[length];
            }

That code seems fine, assuming the old "data" array is being automatically released when you allocate a new one (I don't use .Net myself so I'm not sure :)).

You can't be absolutely certain what the maximum "length" will be, except that it will be less than the playback buffer length (BASS_CONFIG_BUFFER).

2. I kept it stereo and used 220 to calculate the frequency (instead of 440) because later on I will need to deal with panning and such. Am I using stereo correctly? I'm not quite sure about how to pan anything with the way I coded it - I do NOT want to use channel panning, btw.

For stereo, you should duplicate each sample on the left and right. If you want, you can then apply panning to each of them. Something like this...

Code: [Select]
for (int a=0; a<length; a+=2) {
float s=...; // generate the sample
if (pan<0) { // panned left (-1<=pan<=+1)
data[a]=s; // left
data[a+1]=s*(1+pan); // right (attenuated)
} else {
data[a]=s*(1-pan); // left (attenuated)
data[a+1]=s; // right
}
}

3. Can you give me any insight as to whether or not you think this is a good idea: I'm considering generating one period of a sine wave in very tiny increments and storing it in an array when the program starts - then I can sacrifice memory for speed by not having to perform any "live" sine calculations. My concern is about slight inaccuracies that could potentially result from doing this.

I would generate it live for greater accuracy as the CPU usage will be negligible, assuming that you don't need to generate many sinewaves together.

AName

  • Posts: 20
Re: BASS.Net - Unable to output "perfect" sine waves
« Reply #7 on: 23 Mar '11 - 20:11 »
Code: [Select]
That code seems fineI wasn't entirely clear - I meant that instead of calling that code, I wanted to pre-initialize the data array so as to prevent allocating new memory in the callback function.

Code: [Select]
You can't be absolutely certain what the maximum "length" will be, except that it will be less than the playback buffer length (BASS_CONFIG_BUFFER).So I CAN be certain what the maximum length will be. How do I know how to calculate the length that I keep getting when I run my program, 35280? I've figured out that 35280 = (16 (bit?) * .5 (seconds) * 44100 (sample rate)) / 10 (?). Do you know how that works? I found this link: http://www.un4seen.com/forum/?topic=42.msg286#msg286 - that gave me the general idea, but I would like to know for sure.

Code: [Select]
I would generate it live for greater accuracy as the CPU usage will be negligible, assuming that you don't need to generate many sinewaves together.That's a poor (wrong) assumption. :P So, now what would you say about pre-generating waveforms with very small increments? Could accuracy be negligible as well since everything gets rounded off anyways?

Ian @ un4seen

  • Administrator
  • Posts: 26172
Re: BASS.Net - Unable to output "perfect" sine waves
« Reply #8 on: 24 Mar '11 - 15:22 »
So I CAN be certain what the maximum length will be. How do I know how to calculate the length that I keep getting when I run my program, 35280? I've figured out that 35280 = (16 (bit?) * .5 (seconds) * 44100 (sample rate)) / 10 (?). Do you know how that works? I found this link: http://www.un4seen.com/forum/?topic=42.msg286#msg286 - that gave me the general idea, but I would like to know for sure.

You can get the playback buffer size like this (eg. immediately after the BASS_StreamCreate call)...

Code: [Select]
DWORD buflen=BASS_ChannelSeconds2Bytes(handle, BASS_GetConfig(BASS_CONFIG_BUFFER)/1000.0);

BASS doesn't currently ever ask a STREAMPROC for an entire playback buffer of data in one go, but if you want to be safe in preallocating a processing buffer, that is the size you should use for it. Note this only applies to normal playback streams, not decoding channels (created with BASS_STREAM_DECODE).

So, now what would you say about pre-generating waveforms with very small increments? Could accuracy be negligible as well since everything gets rounded off anyways?

It is really up to you. If you're looking for an example of using pre-generated waveforms, you can find one in the SYNTH example (in the BASS package).

AName

  • Posts: 20
Re: BASS.Net - Unable to output "perfect" sine waves
« Reply #9 on: 29 Mar '11 - 02:23 »
Ok, it worked! Here's my final code for this post - it's going to be a gold mine for anyone who's interested in using Bass.NET in a similar way as I am:

Code: [Select]
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using Un4seen.Bass;
using System.Runtime.InteropServices;

namespace Custom_Studio
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        STREAMPROC streamCreate;
        long bufferLength;

        const double twoPI = 6.283185;
        int sampleRate = 44100;

        int channel;

        short[] data; //buffer

        double frequency;
        double increment;
        double wavePosition = 0;

        double frequency2;
        double increment2;
        double wavePosition2 = 0;

        private void playSine()
        {
            //# of half steps UP from A4 (use - to go down)
            int halfSteps = 5;

            frequency = 220d * Math.Pow(2d, ((double)halfSteps / 12d));
            increment = twoPI * frequency / (double)sampleRate;

            frequency2 = 220d * Math.Pow(2d, (((double)halfSteps + 4) / 12d));
            increment2 = twoPI * frequency2 / (double)sampleRate;

            Bass.BASS_ChannelPlay(channel, false);
        }

        private int WaveformProc(int handle, IntPtr buffer, int length, IntPtr user)
        {
            int waveCalculated = length;
            //waveCalculated |= (int)BASSStreamProc.BASS_STREAMPROC_END;

            length /= 2;

            for (int a = 0; a < length; a++)
            {
                data[a] = (short)((2000.0 * Math.Sin(wavePosition)) + (2000.0 * Math.Sin(wavePosition2))); //32767.0

                if (data[a] > 5000)
                {
                    data[a] = 5000;
                }

                data[a + 1] = data[a];

                wavePosition += increment;
                if (wavePosition > twoPI)
                {
                    wavePosition -= twoPI;
                }

                wavePosition2 += increment2;
                if (wavePosition2 > twoPI)
                {
                    wavePosition2 -= twoPI;
                }
            }

            Marshal.Copy(data, 0, buffer, length);

            return waveCalculated;
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            if (Bass.BASS_Init(-1, sampleRate, BASSInit.BASS_DEVICE_DEFAULT, IntPtr.Zero))
            {
                streamCreate = new STREAMPROC(WaveformProc);
                channel = Bass.BASS_StreamCreate(sampleRate, 2, BASSFlag.BASS_DEFAULT, streamCreate, IntPtr.Zero);

                bufferLength = Bass.BASS_ChannelSeconds2Bytes(channel, (double)Bass.BASS_GetConfig(BASSConfig.BASS_CONFIG_BUFFER) / 1000d);
                data = new short[bufferLength];

                playSine();
            }
        }
    }
}

Again, thank you for your help; providing detailed, one-on-one help like this is the best way for you to make Bass popular and user friendly.

I could not find a Synth example for .NET, and I'm terrible at figuring out how programs in a different language function. Perhaps you could use my code or something similar in the next release.

Basti

  • Posts: 12
Re: BASS.Net - Unable to output "perfect" sine waves
« Reply #10 on: 10 Jun '11 - 15:19 »
Hi

Thanks for this great piece of Code! :)
It deals exactly the problem I'm encountering at the moment.

I'm trying to program a DTMF-Generator.
So what I need is some code that is recieving a DTMF-sign (e.g. '3'), generating the appropriate tone consisting of 2 sine-frequencies with a given length and storing that in a wav-file.

The easiest way would be to simply copy your code and don't ask why it's working. But i would like to understand the maths behind the tone-generation.

The basics with sound-theory (distance between semitones/half-tones) is pretty clear. So i generally understand the calculations in the "playSine()"-Method.

But there are some questions left:

1. In the comments given in the code it is mentioned that halfsteps are done from tone A4 (concert pitch). But why is the first sine frequency calcualted with 220Hz as base frequency? That would be an A3. Or am I missing something?

2. I'm not quite sure if i understand the single steps in the code correctly (in WaveformProc there are unfortunatly very few comments), so I will try my own explanation. Please correct my explanations if they are wrong:

In the "data"-array (with twice the number of 'buckets' as the samplerate (44100) because of two channels (stereo)) you store the added (and amplified?) amplitudes of both of the sine-waves at the current "sample-bucket". And you set a maximum amplitude of 5000 (ike a software audio-compressor?) for the added amplitudes?
These steps performed over and over again and if the buffer is "full" (waveposition >twoPi) you start a the beginning of the data-buffer.
after buffer was written completly, there is a Marshal.copy form the dataa-buffer to the "Pointer-buffer" of WaveformProc, so that bass can play that data in BASS_ChanellPlay().

Was this the right explanation of the data-flow?


3. If i want to write that whole produced data to a wav-file, is the right way some kind of this?
Code: [Select]
_waveWriter = new WaveWriter( "test.wav", channel, 24, true);


Thanks i advance for any advice!

Basti


edit:
After working some time with this code example from AName, I figured out a little mistake in his code.
In Waveform-Process the for-loop should count up "+2" and not "a++" (due to stereo channel). Code looks then something like this:
Code: [Select]
        private int WaveformProc(int handle, IntPtr buffer, int length, IntPtr user)
        {
            //store original length of buffer
            int waveCalculated = length;

            //fill SineData-Array with values of the two added sine waves
            for (int a = 0; a < length; a+=2)
            {
                 ...
            }

            //copy added sine data to g_SineChannel if there are still samples to calculate
            Marshal.Copy(g_SineData, 0, buffer, length/2);

            //copy data to wavewriter
            g_WaveWriter.Write(g_SineData, waveCalculated);

            //return length of buffer
            return waveCalculated;
        }

With this correction there is also no dividing of frequency with factor "2" neccessary.

Anyway: Still stays a great piece od code AName. Saved much time for me :)
« Last Edit: 4 Jul '11 - 14:31 by Basti »

AName

  • Posts: 20
Re: BASS.Net - Unable to output "perfect" sine waves
« Reply #11 on: 29 Feb '12 - 00:25 »
Sorry for a much delayed reply, Basti. I just started working on this code again, so I went back to this topic and fixed my mistakes. For starters, here's the new code that works:

Code: [Select]
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Un4seen.Bass;
using System.Runtime.InteropServices;

namespace BASS_Sine_Wave_Test
{
    class Program
    {
        static STREAMPROC streamCreate;
        static long bufferLength;

        const double twoPI = 6.283185;
        static int sampleRate = 44100;

        static int channel;

        static short[] data; //buffer

        static double frequency;
        static double increment;
        static double wavePosition = 0;

        static double frequency2;
        static double increment2;
        static double wavePosition2 = 0;

        static void Main(string[] args)
        {
            if (Bass.BASS_Init(-1, sampleRate, BASSInit.BASS_DEVICE_DEFAULT, IntPtr.Zero))
            {
                streamCreate = new STREAMPROC(WaveformProc);
                channel = Bass.BASS_StreamCreate(sampleRate, 2, BASSFlag.BASS_DEFAULT, streamCreate, IntPtr.Zero);

                bufferLength = Bass.BASS_ChannelSeconds2Bytes(channel, (double)Bass.BASS_GetConfig(BASSConfig.BASS_CONFIG_BUFFER) / 1000d);
                data = new short[bufferLength];

                playSine();
            }
            else
            {
                Console.WriteLine("FAIL");
            }

            Console.ReadLine();
        }

        private static void playSine()
        {
            //# of half steps UP from A4 (use - to go down)
            int halfSteps = 5; //5 half steps up = play D and F#

            frequency = 440d * Math.Pow(2d, ((double)halfSteps / 12d));
            increment = twoPI * frequency / (double)sampleRate;

            frequency2 = 440d * Math.Pow(2d, (((double)halfSteps + 4) / 12d));
            increment2 = twoPI * frequency2 / (double)sampleRate;

            Bass.BASS_ChannelPlay(channel, false);
        }

        private static int WaveformProc(int handle, IntPtr buffer, int length, IntPtr user)
        {
            int waveCalculated = length;
            //waveCalculated |= (int)BASSStreamProc.BASS_STREAMPROC_END;

            length /= 2;

            for (int a = 0; a < length; a += 2)
            {
                data[a] = (short)((16383d * Math.Sin(wavePosition)) + (16383d * Math.Sin(wavePosition2))); //Max total = 32767d

                //Max total amplitude = 32767
                if (data[a] > 32767)
                {
                    data[a] = 32767;
                }

                //Stereo
                data[a + 1] = data[a];

                wavePosition += increment;
                wavePosition2 += increment2;

                if (wavePosition > twoPI)
                {
                    wavePosition -= twoPI;
                }

                if (wavePosition2 > twoPI)
                {
                    wavePosition2 -= twoPI;
                }
            }

            Marshal.Copy(data, 0, buffer, length);

            return waveCalculated;
        }
    }
}


Just for the record: 3 other things need to be done behind the scenes for this code to work:

1. Get the "bass.dll" from the regular BASS package (in the first level inside the "bass24" folder) and import it into the project through the Solution Explorer.

2. Select the "bass.dll" file in the Solution Explorer, and in the Properties window set "Copy to Output Directory" to "Copy if newer".

3. Install BASS.NET (different than the standard BASS package download files) and install it with the automatic option to show BASS in the references in C# (I don't remember what it said exactly, but you'll know it when you see it). Then right click "References" in the Solution Explorer and click "Add Reference". In the ".NET" tab you should see the "BASS.NET API for .Net" component - add it to your project.


Now, regarding what Basti said... Yes, he was right - I needed to use "a += 2" instead of "a++", and I also needed to use "frequency = 440d" instead of "frequency = 220d" (in the playSine method) to accommodate for the fixed mistake.

Yes, Basti, your explanation of the data flow seems correct. The signals must be amplified before they are played or you won't hear them, obviously. :P 32767 is the maximum total peak that BASS should be provided with, otherwise it will distort the sound. So if you wanted to do e.g. 2 signals at the same volume with a total peak below 32767, set their volumes to 16383 like I did.

The reason I didn't comment about anything was because I only make comments when I understand the code myself. ;)

I also have no idea about the Wave Writer, as I haven't used it yet.

Glad my code helped! :)

Basti

  • Posts: 12
Re: BASS.Net - Unable to output "perfect" sine waves
« Reply #12 on: 29 Feb '12 - 08:46 »
Hi AName

Thanks for your assurement to my descriptions/corrections. Even if they were a little bit late :)
I#m sure they will help other people, too.

The Bass-Project, BASS.NET, the forum and, of course, your code AName were all a big help during my diploma thesis last year.
I think without all the help, the mark wouldn't have been quite satisfying as it was/is. ;)

Especially great thanks again to Ian and Bernd Niedergassen for their tireless efforts and help :)

Best regards,
Basti

AName

  • Posts: 20
Re: BASS.Net - Unable to output "perfect" sine waves
« Reply #13 on: 29 Feb '12 - 18:32 »
Ehh, after asking Ian about maybe using my code and instructions as a formal .NET BASS wave stream example, he critiqued one section of my code:

Quote
One thing I noticed is that you don't really need this (because the previous line has a limit of 2*16383)...

Code: [Select]
               //Max total amplitude = 32767
                if (data[a] > 32767)
                {
                    data[a] = 32767;
                }


Note that code wouldn't actually work if the sample data could go out of range because the "data" array is 16-bit, ie. it can't hold anything higher than 32767 (or lower than -32768). What you would end up with is overflows, eg. 32768 would become 0, 32769 would become 1, etc. To properly clip the sample data, you would need to use a larger type (eg. 32-bit integer) to hold the sample and only put the final (post-clipping) result in the "data" array.

He is, of course, right. I replied saying that I made the mistake because I'm used to working with integers, not shorts, so the limit is usually much higher than 32767 and I would need to apply a limit only if I used an integer. That being said, I should point out that if it's possible to have an overflow, you will need to implement a way to prevent that. I can think of only two:

1. Use an integer, and before (explicitly) converting it to a short apply the 32767 cap. I'm concerned with speed and will be using a lot of waves in combination so I am going to take this approach.

2. If the current signal level is e.g. at 30000, take 32767 - 30000 = 2767, and compare 2767 with the signal you are going to add. If 2767 is less than your new signal (e.g. if the new signal is 2768 or higher), then when the current signal and the new signal are added together it will be greater than 32767 and the resulting signal level should simply be 32767. Otherwise, they won't overflow and you can simply add them as before.

In algebraic/procedural terms:
  • C = current signal level
  • S = level of signal being added to C
  • T = C + S
  • T cannot be greater than 32767 because it is a short
  • Before adding, make sure to check that 32767 - C <= S
  • If 32767 - C > S, then T = 32767
  • If 32767 - C <= S, then T = C + S


So, for the 258,934,673,467th time (sorry, I'm a perfectionist), here's the better version:

Code: [Select]
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Un4seen.Bass;
using System.Runtime.InteropServices;

namespace BASS_Sine_Wave_Test
{
    class Program
    {
        static STREAMPROC streamCreate;
        static long bufferLength;

        const double twoPI = 6.283185;
        static int sampleRate = 44100;

        static int channel;

        static short[] data; //buffer

        static double frequency;
        static double increment;
        static double wavePosition = 0;

        static double frequency2;
        static double increment2;
        static double wavePosition2 = 0;

        static void Main(string[] args)
        {
            if (Bass.BASS_Init(-1, sampleRate, BASSInit.BASS_DEVICE_DEFAULT, IntPtr.Zero))
            {
                streamCreate = new STREAMPROC(WaveformProc);
                channel = Bass.BASS_StreamCreate(sampleRate, 2, BASSFlag.BASS_DEFAULT, streamCreate, IntPtr.Zero);

                bufferLength = Bass.BASS_ChannelSeconds2Bytes(channel, (double)Bass.BASS_GetConfig(BASSConfig.BASS_CONFIG_BUFFER) / 1000d);
                data = new short[bufferLength];

                playSine();
            }
            else
            {
                Console.WriteLine("FAIL");
            }

            Console.ReadLine();
        }

        private static void playSine()
        {
            //# of half steps UP from A4 (use - to go down)
            int halfSteps = 5; //5 half steps up = play D and F#

            frequency = 440d * Math.Pow(2d, ((double)halfSteps / 12d));
            increment = twoPI * frequency / (double)sampleRate;

            frequency2 = 440d * Math.Pow(2d, (((double)halfSteps + 4) / 12d));
            increment2 = twoPI * frequency2 / (double)sampleRate;

            Bass.BASS_ChannelPlay(channel, false);
        }

        private static int WaveformProc(int handle, IntPtr buffer, int length, IntPtr user)
        {
            int waveCalculated = length;
            //waveCalculated |= (int)BASSStreamProc.BASS_STREAMPROC_END;

            length /= 2;

            int temporarySignalSum;

            for (int a = 0; a < length; a += 2)
            {
                temporarySignalSum = 16383d * Math.Sin(wavePosition) + 16383d * Math.Sin(wavePosition2); //Max total = 32767d
                
                //Max total amplitude = 32767
                if (temporarySignalSum > 32767)
                {
                    temporarySignalSum = 32767;
                }

                //Stereo
                data[a] = (short)temporarySignalSum;
                data[a + 1] = data[a];

                wavePosition += increment;
                wavePosition2 += increment2;

                if (wavePosition > twoPI)
                {
                    wavePosition -= twoPI;
                }

                if (wavePosition2 > twoPI)
                {
                    wavePosition2 -= twoPI;
                }
            }

            Marshal.Copy(data, 0, buffer, length);

            return waveCalculated;
        }
    }
}

Pflichtfeld

  • Posts: 45
Re: BASS.Net - Unable to output "perfect" sine waves
« Reply #14 on: 8 Aug '13 - 10:03 »
Thanks for this snippets.
Would like to add example vor VB.Net people:

Code: [Select]
#Region "variablendefs"
Private mv As modulvariable
Private Structure modulvariable
         Dim channel1 As Integer
         Dim sampleRate As Integer
         Dim data() As Short
         Dim increment As Double
         Dim increment2 As Double
         Dim volume As Single
         Dim freq1 As Integer
         Dim freq2 As Integer
      End Structure
#End Region

Private streamCreate As STREAMPROC

      Public Sub Startsinus(frequenz1 As Integer, Optional frequenz2 As Integer = 0, Optional volum As Single = 0.4)
         Dim bufferlength As Long
         mv.freq1 = frequenz1
         If frequenz1 <= 0 Then Exit Sub
         If frequenz2 > 0 Then mv.freq2 = frequenz2 Else mv.freq2 = mv.freq1
         mv.volume = volum
         If mv.volume > 1 Then mv.volume = 1

         streamCreate = New STREAMPROC(AddressOf WaveFormProc)
         mv.sampleRate = 44100
         mv.channel1 = Bass.BASS_StreamCreate(mv.sampleRate, 2, BASSFlag.BASS_DEFAULT, streamCreate, IntPtr.Zero)
         bufferlength = Bass.BASS_ChannelSeconds2Bytes(mv.channel1, CDbl(Bass.BASS_GetConfig(BASSConfig.BASS_CONFIG_BUFFER)) / 1000.0)
         mv.data = New Short(bufferlength - 1) {}
         mv.increment = Math.PI * 2 * mv.freq1 / CDbl(mv.sampleRate)
         mv.increment2 = Math.PI * 2 * mv.freq2 / CDbl(mv.sampleRate)
         Bass.BASS_ChannelPlay(mv.channel1, False)
      End Sub


     
      Private Function WaveFormProc(handle As Integer, buffer As IntPtr, length As Integer, user As IntPtr) As Integer
         Dim waveCalculated As Integer = length
         'Static is necessary, because length differs..->clicks
         Static wavePosition, wavePosition2 As Double
         Dim ii, temporarySignalSum As Integer

         length /= 2 'convert bytes to samples (16-bit mono = 2 bytes)
         For ii = 0 To length - 1 Step 2
            temporarySignalSum = mv.volume * (16383.0 * Math.Sin(wavePosition) + 16383.0 * Math.Sin(wavePosition2))
            'Max total amplitude = 32767
            If temporarySignalSum > 32767 Then
               temporarySignalSum = 32767
            End If

            mv.data(ii) = CShort(temporarySignalSum)
            'copy sine data from first stereo-channel to second
            mv.data(ii + 1) = mv.data(ii)
            wavePosition += mv.increment
            wavePosition2 += mv.increment2
            If wavePosition > Math.PI * 2 Then
               wavePosition -= Math.PI * 2
            End If
            If wavePosition2 > Math.PI * 2 Then
               wavePosition2 -= Math.PI * 2
            End If
         Next ii
         Runtime.InteropServices.Marshal.Copy(mv.data, 0, buffer, length)
         Return waveCalculated
      End Function

(I prefer that way with the mv.*-Variables because it makes it easy to access my variables with help of intellisense).

Re: BASS.Net - Unable to output "perfect" sine waves
« Reply #15 on: 31 Aug '23 - 18:14 »
Ehh, after asking Ian about maybe using my code and instructions as a formal .NET BASS wave stream example, he critiqued one section of my code:

Quote
One thing I noticed is that you don't really need this (because the previous line has a limit of 2*16383)...

Code: [Select]
                //Max total amplitude = 32767
                if (data[a] > 32767)
                {
                    data[a] = 32767;
                }


Note that code wouldn't actually work if the sample data could go out of range because the "data" array is 16-bit, ie. it can't hold anything higher than 32767 (or lower than -32768). What you would end up with is overflows, eg. 32768 would become 0, 32769 would become 1, etc. To properly clip the sample data, you would need to use a larger type (eg. 32-bit integer) to hold the sample and only put the final (post-clipping) result in the "data" array.

He is, of course, right. I replied saying that I made the mistake because I'm used to working with integers, not shorts, so the limit is usually much higher than 32767 and I would need to apply a limit only if I used an integer. That being said, I should point out that if it's possible to have an overflow, you will need to implement a way to prevent that. I can think of only two:

1. Use an integer, and before (explicitly) converting it to a short apply the 32767 cap. I'm concerned with speed and will be using a lot of waves in combination so I am going to take this approach.

2. If the current signal level is e.g. at 30000, take 32767 - 30000 = 2767, and compare 2767 with the signal you are going to add. If 2767 is less than your new signal (e.g. if the new signal is 2768 or higher), then when the current signal and the new signal are added together it will be greater than 32767 and the resulting signal level should simply be 32767. Otherwise, they won't overflow and you can simply add them as before.

In algebraic/procedural terms:
  • C = current signal level
  • S = level of signal being added to C
  • T = C + S
  • T cannot be greater than 32767 because it is a short
  • Before adding, make sure to check that 32767 - C <= S
  • If 32767 - C > S, then T = 32767
  • If 32767 - C <= S, then T = C + S


So, for the 258,934,673,467th time (sorry, I'm a perfectionist), here's the better version:

Code: [Select]
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Un4seen.Bass;
using System.Runtime.InteropServices;

namespace BASS_Sine_Wave_Test
{
    class Program
    {
        static STREAMPROC streamCreate;
        static long bufferLength;

        const double twoPI = 6.283185;
        static int sampleRate = 44100;

        static int channel;

        static short[] data; //buffer

        static double frequency;
        static double increment;
        static double wavePosition = 0;

        static double frequency2;
        static double increment2;
        static double wavePosition2 = 0;

        static void Main(string[] args)
        {
            if (Bass.BASS_Init(-1, sampleRate, BASSInit.BASS_DEVICE_DEFAULT, IntPtr.Zero))
            {
                streamCreate = new STREAMPROC(WaveformProc);
                channel = Bass.BASS_StreamCreate(sampleRate, 2, BASSFlag.BASS_DEFAULT, streamCreate, IntPtr.Zero);

                bufferLength = Bass.BASS_ChannelSeconds2Bytes(channel, (double)Bass.BASS_GetConfig(BASSConfig.BASS_CONFIG_BUFFER) / 1000d);
                data = new short[bufferLength];

                playSine();
            }
            else
            {
                Console.WriteLine("FAIL");
            }

            Console.ReadLine();
        }

        private static void playSine()
        {
            //# of half steps UP from A4 (use - to go down)
            int halfSteps = 5; //5 half steps up = play D and F#

            frequency = 440d * Math.Pow(2d, ((double)halfSteps / 12d));
            increment = twoPI * frequency / (double)sampleRate;

            frequency2 = 440d * Math.Pow(2d, (((double)halfSteps + 4) / 12d));
            increment2 = twoPI * frequency2 / (double)sampleRate;

            Bass.BASS_ChannelPlay(channel, false);
        }

        private static int WaveformProc(int handle, IntPtr buffer, int length, IntPtr user)
        {
            int waveCalculated = length;
            //waveCalculated |= (int)BASSStreamProc.BASS_STREAMPROC_END;

            length /= 2;

            int temporarySignalSum;

            for (int a = 0; a < length; a += 2)
            {
                temporarySignalSum = 16383d * Math.Sin(wavePosition) + 16383d * Math.Sin(wavePosition2); //Max total = 32767d
               
                //Max total amplitude = 32767
                if (temporarySignalSum > 32767)
                {
                    temporarySignalSum = 32767;
                }

                //Stereo
                data[a] = (short)temporarySignalSum;
                data[a + 1] = data[a];

                wavePosition += increment;
                wavePosition2 += increment2;

                if (wavePosition > twoPI)
                {
                    wavePosition -= twoPI;
                }

                if (wavePosition2 > twoPI)
                {
                    wavePosition2 -= twoPI;
                }
            }

            Marshal.Copy(data, 0, buffer, length);

            return waveCalculated;
        }
    }
}

The code you provided doesn't actually provide a stereo output. By setting data[a + 1] to data[a], you're creating a mono output. It may look redundant, but data[a + 1] must be given its own data instead of taking from the other speaker.
Code: [Select]
private static int WaveformProc(int handle, IntPtr buffer, int length, IntPtr user)
{
    int waveCalculated = length;
    //waveCalculated |= (int)BASSStreamProc.BASS_STREAMPROC_END;

    length /= 2;
    if (data == null)
        throw new ArgumentNullException(nameof(data), "The buffer array is null");

    for (int a = 0; a < length; a += 2)
    {

        data[a] = (short)(32767 * Math.Sin(wavePosition)); //Right speaker
        data[a + 1] = (short)(32767 * Math.Sin(wavePosition2)); //Left speaker


            wavePosition +=increment;

            if (wavePosition > twoPI)
            {
                wavePosition -= twoPI;
            }

            wavePosition2 += increment2;

            if (wavePosition2 > twoPI)
            {
                wavePosition2 -= twoPI;
            }
    }

    Marshal.Copy(data, 0, buffer, length);

    return waveCalculated;
}