Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

waveOutPrepareHeader error in Blackthorne #270

Open
Vort opened this issue Feb 14, 2024 · 8 comments
Open

waveOutPrepareHeader error in Blackthorne #270

Vort opened this issue Feb 14, 2024 · 8 comments

Comments

@Vort
Copy link
Contributor

Vort commented Feb 14, 2024

When playing Blackthorne game on Windows host, race condition in sound code may happen.
It results in one of three consequences:

  1. waveOutPrepareHeader(): error = 5 messages are displayed.
  2. Sound output stops (was triggered in the video).
  3. Bochs crashes.

Problem reproduces most reliably in such scenario:

  1. Orc shoots player.
  2. Orc starts laughing.
  3. Player jumps up the ledge.
bochs_bthorne.mp4

During its execution, game switches between 11kHz and 22kHz output, which generates race condition between bx_soundlow_waveout_win_c::set_pcm_params (waveOutOpen) and bx_soundlow_waveout_win_c::output (waveOutPrepareHeader) functions.

Test files: bthorne.zip.
Version: 1ff88fd.

vruppert added a commit that referenced this issue Apr 1, 2024
If resampler is not present, flush output before changing PCM parameters.
@vruppert
Copy link
Contributor

vruppert commented Apr 1, 2024

Please try again with latest code.

@Vort
Copy link
Contributor Author

Vort commented Apr 1, 2024

I still see problems # 1 and # 2 with 2979ba4. I wasn't able to get crash however.

@Vort
Copy link
Contributor Author

Vort commented Apr 2, 2024

Race condition happens at exactly the same place as before.
Most likely, because of samples made by fmopl_generator.

@Vort
Copy link
Contributor Author

Vort commented Apr 2, 2024

I think the only proper long-term solution is to integrate resample code in a way similar to softfloat.
In short-term, however, some hacks may be available, not sure.

@vruppert
Copy link
Contributor

vruppert commented Apr 3, 2024

Yes, my fix only handles the PCM part of the sound emulation. I'll have a look how to stop the FM and speaker input of the mixer code. You are right, the real bugfix for all platforms would be some resample code in Bochs. Since the default sample rate is 44100 Hz (CD audio quality), resampling for 11025 and 22050 Hz should be relatively easy to implement.

@Vort
Copy link
Contributor Author

Vort commented Apr 3, 2024

Blackthorne uses not 11025 and 22050, but 11111 and 22222.
Also I noticed unusual sample rates in the past while was testing various other software.
For example, Impulse Tracker uses 45454.
So there will be no easy way I guess.

@Vort
Copy link
Contributor Author

Vort commented Apr 3, 2024

Arbitrary resampling can be done with filter banks somehow.
Some info is here (and in code of GNU Radio of course). Book mentioned there is here (archive), chapter 7.5 starts at page 171 (187).
I tried this algorithm in other project and it worked, but I forgot already all details (math knowledge disappears from my brain very quickly for some reason).

@Vort
Copy link
Contributor Author

Vort commented Apr 4, 2024

I decided to make small program demonstrating arbitrary resampling.
Here is source code ArbResamp_src.zip, binary and data ArbResamp_bin.zip.
Program is made in C#, but I hope it is similar enough to C++ to be understood properly.

Source code
using System;
using System.Collections.Generic;
using System.IO;

namespace ArbResamp
{
    class Program
    {
        Program()
        {
            Resample("sine_1000", 2222, 3000);
            Resample("bt33", 11111, 44100);
        }

        void Resample(string fileIn, double sampleRate1, double sampleRate2)
        {
            Console.Write("Resampling " + fileIn + "...");

            double[] inData = SignalStoD(LoadWav(fileIn + ".wav"));

            double intCounter = 0.0;
            double intAdjust = sampleRate1 / sampleRate2;

            const int filterLength = 511;
            double k = 2 * Math.PI / (filterLength + 1);
            int n = filterLength / 2;

            var resampled = new List<double>();
            for (int i = 0; i < inData.Length; i++)
            {
                while (intCounter < 1.0)
                {
                    double sum = 0;
                    double t = -n - intCounter;
                    for (int j = 0; j < filterLength; j++)
                    {
                        double lpf = 1.0;
                        if (t != 0.0)
                            lpf = Sinc(t * Math.PI);
                        double inSample = 0.0;
                        int sampleIndex = i + j - n;
                        if (sampleIndex >= 0 && sampleIndex < inData.Length)
                            inSample = inData[sampleIndex];
                        double hannWindow = 0.5 - 0.5 * Math.Cos(k * (j + 1));
                        sum += lpf * hannWindow * inSample;
                        t++;
                    }
                    if (sum < -1.0)
                        sum = -1.0;
                    else if (sum > 1.0)
                        sum = 1.0;
                    resampled.Add(sum);
                    intCounter += intAdjust;
                }
                intCounter -= 1.0;
            }

            SaveWav(fileIn + "_rs.wav", SignalDtoS(resampled.ToArray(), true), (int)sampleRate2);
            Console.WriteLine(" Done");
        }

        double Sinc(double x)
        {
            if (x == 0.0)
                return 1.0;
            return Math.Sin(x) / x;
        }

        double[] SignalStoD(short[] signal)
        {
            double[] signalD = new double[signal.Length];
            for (int i = 0; i < signal.Length; i++)
                signalD[i] = signal[i] / 32768.0;
            return signalD;
        }

        short[] SignalDtoS(double[] signal, bool dither = false)
        {
            Random rnd = new Random(12345);
            short[] signalS = new short[signal.Length];
            if (dither)
            {
                for (int i = 0; i < signal.Length; i++)
                {
                    double r = (rnd.NextDouble() + rnd.NextDouble()) - 1;
                    signalS[i] = Convert.ToInt16(signal[i] * 32766.0 + r);
                }
            }
            else
            {
                for (int i = 0; i < signal.Length; i++)
                    signalS[i] = Convert.ToInt16(signal[i] * 32767.0);
            }
            return signalS;
        }

        short[] LoadWav(string path)
        {
            FileStream fs = File.Open(path, FileMode.Open);
            long sampleCount = (fs.Length - 0x2C) / 2;
            fs.Seek(0x2C, SeekOrigin.Begin);

            byte[] smpBuf = new byte[2];
            short[] signal = new short[sampleCount];
            for (int i = 0; i < sampleCount; i++)
            {
                fs.Read(smpBuf, 0, 2);
                signal[i] = BitConverter.ToInt16(smpBuf, 0);
            }
            fs.Close();
            return signal;
        }


        void SaveWav(string path, short[] signal, int sampleRate)
        {
            FileStream fs = File.Open(path, FileMode.Create);
            fs.Write(new byte[] { 0x52, 0x49, 0x46, 0x46 }, 0, 4); // "RIFF"
            fs.Write(BitConverter.GetBytes((uint)(36 + signal.Length * 2)), 0, 4);
            fs.Write(new byte[] { 0x57, 0x41, 0x56, 0x45 }, 0, 4); // "WAVE"
            fs.Write(new byte[] { 0x66, 0x6D, 0x74, 0x20 }, 0, 4); // "fmt"
            fs.Write(BitConverter.GetBytes((uint)(16)), 0, 4);
            fs.Write(BitConverter.GetBytes((ushort)(1)), 0, 2);
            fs.Write(BitConverter.GetBytes((ushort)(1)), 0, 2); // mono
            fs.Write(BitConverter.GetBytes((uint)(sampleRate)), 0, 4); // Hz
            fs.Write(BitConverter.GetBytes((uint)(sampleRate * 2)), 0, 4);
            fs.Write(BitConverter.GetBytes((ushort)(2)), 0, 2);
            fs.Write(BitConverter.GetBytes((ushort)(16)), 0, 2); // bps
            fs.Write(new byte[] { 0x64, 0x61, 0x74, 0x61 }, 0, 4); // "data"
            fs.Write(BitConverter.GetBytes((uint)(signal.Length * 2)), 0, 4);
            foreach (short v in signal)
                fs.Write(BitConverter.GetBytes(v), 0, 2);
            fs.Close();
        }

        static void Main(string[] args)
        {
            new Program();
        }
    }
}

Upon execution, it resamples two files:

  1. sine_1000.wav, 1000 Hz sine wave, from 2222 Hz to 3000 Hz sample rate.
  2. bt33.wav, explosion sample from the game (can be heard at 1:15 in video), from 11111 Hz to 44100 Hz.

Quality of resampling can be estimated by looking at spectrograms made by SoX:

sine_1000 (original)

sine_1000

sine_1000_rs (resampled, 511 taps)

sine_1000_rs

bt33 (original)

bt33

bt33_rs (resampled, 511 taps)

bt33_rs

Two effects can be seen there:

  1. Artifacts at the start and the end of sine wave. They happen because resampling algorithm use convolution, which require for each resulting sample to have several input samples. For file it means that samples beyond original data are needed. This problem is "solved" by this line if (sampleIndex >= 0 && sampleIndex < inData.Length). For realtime processing, same property means that output data will have some lag.
  2. At 0.2 seconds mark at bt33_rs spectrogram spike can be seen. It happens because resampled data may have higher amplitude than original data and clipping is used to fit data into 16 bit range: if (sum < -1.0).

These spectrograms were made with const int filterLength = 511;. Having lower tap count, for example, 51 will produce visible artifacts:

sine_1000_rs_51 (resampled, 51 taps)

sine_1000_rs_51

bt33_rs_51 (resampled, 51 taps)

bt33_rs_51

Visible artifacts, however, does not mean audible artifacts, so with this option delay, quality and CPU usage can be controlled at the same time.

Since this program is small, it is also slow. With polyphase filter banks results of costly Sin function calls can be cached, at the cost of slightly worse resampling quality.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants