bochs-emu / Bochs

Bochs - Cross Platform x86 Emulator Project
https://bochs.sourceforge.io/
GNU Lesser General Public License v2.1
853 stars 98 forks source link

waveOutPrepareHeader error in Blackthorne #270

Open Vort opened 7 months ago

Vort commented 7 months ago

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.

https://github.com/bochs-emu/Bochs/assets/1242858/7781460e-de86-4f9f-b9a2-65f94f2cd7bb

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: 1ff88fdd05a9b6640029fa0ba04304d58e833b3e.

vruppert commented 5 months ago

Please try again with latest code.

Vort commented 5 months ago

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

Vort commented 5 months ago

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

Vort commented 5 months ago

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 commented 5 months ago

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 commented 5 months ago

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 commented 5 months ago

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 commented 5 months ago

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 ```cs 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(); 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](https://github.com/bochs-emu/Bochs/assets/1242858/8ed89fed-0eeb-489b-b2d5-2cc0c01c92ba)
sine_1000_rs (resampled, 511 taps) ![sine_1000_rs](https://github.com/bochs-emu/Bochs/assets/1242858/e3bb960a-3110-4913-9eeb-54c770f0e6eb)
bt33 (original) ![bt33](https://github.com/bochs-emu/Bochs/assets/1242858/6429f4c4-ba7b-4c2f-9fbe-f5e196479a21)
bt33_rs (resampled, 511 taps) ![bt33_rs](https://github.com/bochs-emu/Bochs/assets/1242858/4166441f-aed9-48f9-a62d-8bad949cda6d)

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](https://github.com/bochs-emu/Bochs/assets/1242858/345e40be-1070-4944-ab04-d7ad354babc1)
bt33_rs_51 (resampled, 51 taps) ![bt33_rs_51](https://github.com/bochs-emu/Bochs/assets/1242858/121bda04-a2b6-408a-8851-390ded692a1e)

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.