1

My goal is to generate an accurate sine wave. My problem is that when I use BigDecimal and StrictMath to generate the values, some of the zero crossings are wrong and the symmetry is broken.

Here is an array generated with a frequency of 1, a phase of 0, an amplitude of 1, a time of 1 second and a sample rate of 10 (I'll post the code later in this post):

>[0]    0.0

>[1]    0.5877852522924731  
[2] 0.9510565162951535  
[3] 0.9510565162951536  
[4] 0.5877852522924732  
[5] 1.2246467991473532E-16  
[6] -0.587785252292473  
[7] -0.9510565162951535 

>[8]    -0.9510565162951536 

>[9]    -0.5877852522924734 

Shouldn't [5] be 0 for accuracy? Shouldn't (4 = 1) as well as (2 = 3),(9 = 6) and (7 = 8)?

A 2nd case, where the phase is equal to StrictMath.PI/2.0 appears to produce accuracy at [5]:

>[0]    1.0 

>[1]    0.8090169943749475  
[2] 0.3090169943749475  
[3] -0.3090169943749473 

>[4]    -0.8090169943749473 

>[5]    -1.0    
[6] -0.8090169943749476 

>[7]    -0.3090169943749476 

>[8]    0.3090169943749472  
[9] 0.8090169943749472  

In this case, where the starting point is less accurate , [5] is more accurate, but once again, shouldn't (-4 = 1) as well as (-2 = 3),(-9 = 6) and (-7 = 8)?

So my question is why is this the case? Why are the zero crossings wrong, but the 1 and -1 crossings right? Why is the sine symmetry broken?

Here is my code for generating the values:


    package Wave;

    import java.math.BigDecimal;

    /**
     * @author Alexander Johnston
     * Copyright 2019
     * A class for sine waves
     */
    public class SineWave extends Wave {

        /** Creates a sine wave
         * @param a as the amplitude of the sin wave from -amplitude to amplitude
         * @param f as the frequency of the sine wave in Hz
         * @param p as the phase of the sine wave
         */
        public SineWave(BigDecimal a, BigDecimal f, BigDecimal p) {
            this.a = a;
            this.f = f;
            this.p = p;
        }

        /* (non-Javadoc)
         * @see waves.Wave#getSample(BigDecimal, float)
         */
        public double[] getSample(BigDecimal t, float sr) {
            int nsd;
            BigDecimal nsdp = (new BigDecimal(Float.toString(sr)).multiply(t));
            if(nsdp.compareTo(new BigDecimal(Integer.MAX_VALUE)) == -1) {
                nsd = nsdp.intValue();
                } else {
                    System.out.print("wave time is too long to fit in an array");
                    return null;
                }
            double[] w = new double[nsd];
            for(int i = 0; i < w.length; i++) {
                w[i] = a.multiply(new BigDecimal(StrictMath.sin(((new BigDecimal(2.0).multiply(new BigDecimal(StrictMath.PI)).multiply(f).multiply(new BigDecimal(i)).divide((new BigDecimal(Float.toString(sr))))).add(p)).doubleValue()))).doubleValue();
            }
            p = p.add(new BigDecimal(2.0).multiply(new BigDecimal(StrictMath.PI).multiply(f).multiply(t)));
            return w;
        }
    }


    The wave class:

    package Wave;

    import java.math.BigDecimal;

    /**
     * @author Alexander Johnston
     * Copyright 2019
     * A class for waves to extend
     */
    public abstract class Wave {

        // Amplitude of the wave
        protected BigDecimal a;

        // Frequency of the wave in Hz
        protected BigDecimal f;

        // Phase of the wave, between 0 and (2*Math.PI)
        protected BigDecimal p;

        /** Generates a wave with with the correct amplitude
         * @param t as the length of the wave in seconds
         * @return An array with the wave generated with specified amplitude as amplitude over time
         */
        abstract public double[] getSample(BigDecimal t, float sr);

        }

and the main method:


    import java.math.BigDecimal;
    import Wave.SineWave;

    public class main {

        public static void main(String[] args) {
            BigDecimal a = new BigDecimal(1.0); 
            BigDecimal f = new BigDecimal(1.0); 
            BigDecimal p = new BigDecimal(0.0);
            SineWave sw = new SineWave(a, f, p);        
            p = new BigDecimal(StrictMath.PI).divide(new BigDecimal(2.0));
            SineWave swps = new SineWave(a, f, p);
            BigDecimal t = new BigDecimal(1.0);
            float sr = 10;
        // The first array in this post
            double [] swdns = sw.getSample(t, sr);
        // The second array in this post
            double [] swpsdns = swps.getSample(t, sr);
        }

Thank you for taking the time to look over my post. Your help is greatly appreciated.

Al G Johnston
  • 129
  • 2
  • 10
  • 2
    `StrictMath.sin` doesn't work with BigDecimals - only doubles - and you're constantly converting between BigDecimal and double. How does this improve accuracy? You'd need to find a library (or your own code) to calculate the `sin` function directly into a BigDecimal with the required precision. – Erwin Bolwidt Nov 24 '19 at 23:52
  • I only use conversion where absolutely necessary, The results do give me better accuracy than if I hadn't used BigDecimal. I'll look for a library. Thank you for your help. – Al G Johnston Nov 24 '19 at 23:57
  • 1
    You could look at this question for some suggestions on libraries: https://stackoverflow.com/questions/2173512/java-bigdecimal-trigonometric-methods I think the issues that you're seeing still stem from the limited precision of a `double`. If you just look at fewer decimals, all the equalities that you were expecting are there. For example, `1.2246467991473532E-16` is actually really, really close to zero - the fraction part starts with 15 zeros and only then it has some non-zero digits, they all stem from the expected calculation error. – Erwin Bolwidt Nov 25 '19 at 00:33
  • 1
    I think I need more accuracy because I am working on generating sound waves. In the above case where I use Math.sin, there is drift in the phase calculations and over a period of time this would be become more noticeable as all those small zero crossing errors added together. Maybe I am being picky, after doing some math, this corresponds to an amplitude of 0.00000000000097208748 after 3 minutes. It is good I found a solution though, I was having trouble with accuracy in other parts of my project. – Al G Johnston Nov 25 '19 at 01:46

1 Answers1

1

As Erwin recommended, I found a library that work for my needs. BigDecimalMath It has a generous license and fixed my problem with these particular arrays when I set the accuracy to 1074 decimal places, which is the maximum absolute value of the negative exponent of a Java primitive double value.

Thank you again for your help Erwin!

Al G Johnston
  • 129
  • 2
  • 10