Skip to content

Instantly share code, notes, and snippets.

@kode54
Last active September 21, 2022 05:50
Show Gist options
  • Save kode54/2519179722432fc00be57ffb5155715f to your computer and use it in GitHub Desktop.
Save kode54/2519179722432fc00be57ffb5155715f to your computer and use it in GitHub Desktop.
This fairly simple tool converts all HeSuVi 14 channel presets into 7.0 formatted _L/_R stereo pairs, for use with the soon to be updated PulseAudio module-virtual-surround-sink, which I've updated with a faster FFT overlap-save convolver, eliminated the sample length limits for impulses, and added support for asymmetrical/dual impulse mode.

To convert an impulse from HeSuVi, for example, the base GSX filter:

hesuvi_convert gsx.wav gsx_L.wav gsx_R.wav

And to use it:

pacmd load-module module-virtual-surround-sink sink_name=VirtualSurround \
    sink_master=<output device> hrir_left=$PWD/gsx_L.wav hrir_right=$PWD/gsx_R.wav \
    channel_map=front-left,front-right,rear-left,rear-right,front-center,lfe

(Suggested channel_map override added, since either Wine or some Wine games barf with 7.1 instead of 5.1)

Merge request !240 on pulseaudio/pulseaudio at Freedesktop.org Gitlab

Thread I started on Reddit about this

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#define SPEAKER_FRONT_LEFT 0x1
#define SPEAKER_FRONT_RIGHT 0x2
#define SPEAKER_FRONT_CENTER 0x4
#define SPEAKER_LFE 0x8
#define SPEAKER_BACK_LEFT 0x10
#define SPEAKER_BACK_RIGHT 0x20
#define SPEAKER_SIDE_LEFT 0x200
#define SPEAKER_SIDE_RIGHT 0x400
#define OUTPUTS 8
#define SPEAKERS_HESUVI \
(SPEAKER_FRONT_LEFT| \
SPEAKER_FRONT_RIGHT| \
SPEAKER_FRONT_CENTER| \
SPEAKER_LFE| \
SPEAKER_BACK_LEFT| \
SPEAKER_BACK_RIGHT| \
SPEAKER_SIDE_LEFT| \
SPEAKER_SIDE_RIGHT)
static const uint32_t speakers_output[OUTPUTS] =
{
SPEAKER_FRONT_LEFT,
SPEAKER_FRONT_RIGHT,
SPEAKER_FRONT_CENTER,
SPEAKER_FRONT_CENTER, /* duplicate LFE into center preset */
SPEAKER_BACK_LEFT,
SPEAKER_BACK_RIGHT,
SPEAKER_SIDE_LEFT,
SPEAKER_SIDE_RIGHT,
};
static const uint32_t speakers_hesuvi_to_left[14] =
{
SPEAKER_FRONT_LEFT,
0,
SPEAKER_SIDE_LEFT,
0,
SPEAKER_BACK_LEFT,
0,
SPEAKER_FRONT_CENTER,
0,
SPEAKER_FRONT_RIGHT,
0,
SPEAKER_SIDE_RIGHT,
0,
SPEAKER_BACK_RIGHT,
0,
};
static const uint32_t speakers_hesuvi_to_right[14] =
{
0,
SPEAKER_FRONT_LEFT,
0,
SPEAKER_SIDE_LEFT,
0,
SPEAKER_BACK_LEFT,
0,
SPEAKER_FRONT_RIGHT,
0,
SPEAKER_SIDE_RIGHT,
0,
SPEAKER_BACK_RIGHT,
0,
SPEAKER_FRONT_CENTER,
};
typedef struct __attribute__((packed)) tWAVEFORMATEX {
uint16_t wFormatTag;
uint16_t nChannels;
uint32_t nSamplesPerSec;
uint32_t nAvgBytesPerSec;
uint16_t nBlockAlign;
uint16_t wBitsPerSample;
uint16_t cbSize;
} WAVEFORMATEX, *PWAVEFORMATEX, *NPWAVEFORMATEX, *LPWAVEFORMATEX;
typedef struct __attribute__((packed)) _GUID {
uint32_t Data1;
uint16_t Data2;
uint16_t Data3;
uint8_t Data4[8];
} GUID;
static const GUID KSDATAFORMAT_SUBTYPE_IEEE_FLOAT =
{ 3,
0,
16,
{128, 0, 0, 170, 0, 56, 155, 113}
};
typedef struct __attribute__((packed)) {
WAVEFORMATEX Format;
union {
uint16_t wValidBitsPerSample;
uint16_t wSamplesPerBlock;
uint16_t wReserved;
} Samples;
uint32_t dwChannelMask;
GUID SubFormat;
} WAVEFORMATEXTENSIBLE, *PWAVEFORMATEXTENSIBLE;
static uint16_t get_le16_ptr(const uint8_t * in)
{
return (uint16_t)in[0] + (uint16_t)in[1] * 0x100;
}
static void put_le16_ptr(uint8_t * out, uint16_t value)
{
out[0] = (uint8_t)(value & 0xFF);
out[1] = (uint8_t)((value >> 8) & 0xFF);
}
static uint16_t get_le16(uint16_t in)
{
union {
uint16_t val16;
uint8_t val8[2];
} temp;
temp.val16 = in;
return get_le16_ptr(temp.val8);
}
static uint16_t put_le16(uint16_t in)
{
union {
uint16_t val16;
uint8_t val8[2];
} temp;
put_le16_ptr(temp.val8, in);
return temp.val16;
}
static uint32_t get_le32_ptr(const uint8_t * in)
{
return (uint32_t)in[0] + (uint32_t)in[1] * 0x100 +
(uint32_t)in[2] * 0x10000 + (uint32_t)in[3] * 0x1000000;
}
static void put_le32_ptr(uint8_t * out, uint32_t val)
{
out[0] = (uint8_t)(val & 0xFF);
out[1] = (uint8_t)((val >> 8) & 0xFF);
out[2] = (uint8_t)((val >> 16) & 0xFF);
out[3] = (uint8_t)((val >> 24) & 0xFF);
}
static uint32_t get_le32(uint32_t in)
{
union {
uint32_t val32;
uint8_t val8[4];
} temp;
temp.val32 = in;
return get_le32_ptr(temp.val8);
}
static uint32_t put_le32(uint32_t in)
{
union {
uint32_t val32;
uint8_t val8[4];
} temp;
put_le32_ptr(temp.val8, in);
return temp.val32;
}
static void put_guid_ptr(uint8_t * out, GUID in)
{
put_le32_ptr(out, in.Data1);
put_le16_ptr(out + 4, in.Data2);
put_le16_ptr(out + 6, in.Data3);
memcpy(out + 8, in.Data4, 8);
}
static GUID put_guid(GUID value)
{
union {
GUID valGuid;
uint8_t val8[16];
} temp;
put_guid_ptr(temp.val8, value);
return temp.valGuid;
}
int main(int argc, char ** argv)
{
FILE *f;
uint8_t * input_wav;
uint8_t * output_wav_left, * output_wav_right;
size_t input_size;
size_t output_size;
uint32_t i, j, k, mask;
uint32_t val32;
uint32_t format_size;
uint32_t sample_count;
size_t fact_chunk_offset;
size_t sample_data_offset;
size_t sample_data_size;
size_t sample_out_offset;
PWAVEFORMATEX wavHeader;
WAVEFORMATEXTENSIBLE outHeader = {0};
if (argc != 4) {
fprintf(stderr, "Usage:\thesuvi_convert <input.wav> <output_left.wav> <output_right.wav>\n");
return 1;
}
f = fopen(argv[1], "rb");
if (!f) {
fprintf(stderr, "Unable to open output file: %s\n", argv[1]);
return 1;
}
fseek(f, 0, SEEK_END);
input_size = ftell(f);
fseek(f, 0, SEEK_SET);
input_wav = malloc(input_size + 1);
if (!input_wav) {
fprintf(stderr, "Unable to allocate memory for input file\n");
fclose(f);
return 1;
}
if (fread(input_wav, 1, input_size, f) != input_size) {
fprintf(stderr, "Unable to read input file\n");
free(input_wav);
fclose(f);
return 1;
}
fclose(f);
if (memcmp(input_wav, "RIFF", 4) != 0) {
fprintf(stderr, "Invalid input file signature\n");
free(input_wav);
return 1;
}
val32 = get_le32_ptr(input_wav + 4);
if (val32 + 8 > input_size) {
fprintf(stderr, "Invalid RIFF chunk size in input\n");
free(input_wav);
return 1;
}
if (memcmp(input_wav + 8, "WAVE", 4) != 0) {
fprintf(stderr, "Not a WAV file\n");
free(input_wav);
return 1;
}
if (memcmp(input_wav + 12, "fmt ", 4) != 0) {
fprintf(stderr, "WAV format chunk not where expected\n");
free(input_wav);
return 1;
}
format_size = get_le32_ptr(input_wav + 16);
if (format_size < 16) {
fprintf(stderr, "WAV format chunk not large enough\n");
}
wavHeader = (PWAVEFORMATEX)(input_wav + 20);
if (get_le16(wavHeader->wFormatTag) != 3 ||
get_le16(wavHeader->nChannels) != 14 ||
get_le16(wavHeader->wBitsPerSample) != 32 ||
get_le16(wavHeader->nBlockAlign) != get_le16(wavHeader->nChannels) * 4 ||
get_le32(wavHeader->nSamplesPerSec) != get_le32(wavHeader->nAvgBytesPerSec) / get_le16(wavHeader->nBlockAlign)) {
fprintf(stderr, "WAV has unexpected format\n");
free(input_wav);
return 1;
}
memset(&outHeader, 0, sizeof(outHeader));
memcpy(&outHeader.Format, wavHeader, format_size);
outHeader.Format.wFormatTag = put_le16(0xFFFE);
outHeader.Format.cbSize = put_le16(sizeof(WAVEFORMATEXTENSIBLE) - sizeof(WAVEFORMATEX));
outHeader.Format.nChannels = put_le16(OUTPUTS);
outHeader.Format.nBlockAlign = put_le16(OUTPUTS * 4);
outHeader.Format.nAvgBytesPerSec = put_le32(get_le32(wavHeader->nSamplesPerSec) * OUTPUTS * 4);
outHeader.Samples.wValidBitsPerSample = put_le16(32);
outHeader.SubFormat = put_guid(KSDATAFORMAT_SUBTYPE_IEEE_FLOAT);
outHeader.dwChannelMask = put_le32(SPEAKERS_HESUVI);
if (memcmp(input_wav + 20 + format_size, "fact", 4) != 0) {
fprintf(stderr, "Input WAV is missing fact chunk\n");
free(input_wav);
return 1;
}
val32 = get_le32_ptr(input_wav + 20 + format_size + 4);
if (val32 != 4) {
fprintf(stderr, "Input WAV fact chunk is wrong size\n");
free(input_wav);
return 1;
}
fact_chunk_offset = 20 + format_size;
sample_count = get_le32_ptr(input_wav + fact_chunk_offset + 8);
sample_data_offset = 20 + format_size + 12;
while (sample_data_offset < input_size) {
if (memcmp(input_wav + sample_data_offset, "data", 4) == 0)
break;
val32 = get_le32_ptr(input_wav + sample_data_offset + 4);
sample_data_offset += 8 + val32;
}
if (sample_data_offset >= input_size) {
fprintf(stderr, "WAV data chunk not found\n");
free(input_wav);
return 1;
}
sample_data_size = get_le32_ptr(input_wav + sample_data_offset + 4);
sample_data_offset += 8;
if (sample_data_size < sample_count * 14 * 4) {
fprintf(stderr, "WAV data too small for specified sample count\n");
free(input_wav);
return 1;
}
/* RIFF + fmt + fact + data */
output_size = 8 + 4 + 8 + sizeof(outHeader) + 8 + 4 + 8 + sample_count * OUTPUTS * 4;
output_wav_left = calloc(1, output_size);
if (!output_wav_left) {
fprintf(stderr, "Unable to allocate output buffer\n");
free(input_wav);
return 1;
}
output_wav_right = calloc(1, output_size);
if (!output_wav_right) {
fprintf(stderr, "Unable to allocate output buffer\n");
free(output_wav_left);
free(input_wav);
return 1;
}
memcpy(output_wav_left, "RIFF", 4);
memcpy(output_wav_right, "RIFF", 4);
put_le32_ptr(output_wav_left + 4, output_size - 8);
put_le32_ptr(output_wav_right + 4, output_size - 8);
memcpy(output_wav_left + 8, "WAVE", 4);
memcpy(output_wav_right + 8, "WAVE", 4);
memcpy(output_wav_left + 12, "fmt ", 4);
memcpy(output_wav_right + 12, "fmt ", 4);
put_le32_ptr(output_wav_left + 16, sizeof(outHeader));
put_le32_ptr(output_wav_right + 16, sizeof(outHeader));
memcpy(output_wav_left + 20, &outHeader, sizeof(outHeader));
memcpy(output_wav_right + 20, &outHeader, sizeof(outHeader));
memcpy(output_wav_left + 20 + sizeof(outHeader), input_wav + fact_chunk_offset, 12);
memcpy(output_wav_right + 20 + sizeof(outHeader), input_wav + fact_chunk_offset, 12);
memcpy(output_wav_left + 20 + sizeof(outHeader) + 12, "data", 4);
memcpy(output_wav_right + 20 + sizeof(outHeader) + 12, "data", 4);
put_le32_ptr(output_wav_left + 20 + sizeof(outHeader) + 12 + 4, sample_count * OUTPUTS * 4);
put_le32_ptr(output_wav_right + 20 + sizeof(outHeader) + 12 + 4, sample_count * OUTPUTS * 4);
sample_out_offset = 20 + sizeof(outHeader) + 12 + 8;
for (i = 0; i < sample_count; i++) {
uint32_t * frame_in = (uint32_t *)(input_wav + sample_data_offset + i * 14 * 4);
uint32_t * frame_out_left = (uint32_t *)(output_wav_left + sample_out_offset + i * OUTPUTS * 4);
uint32_t * frame_out_right = (uint32_t *)(output_wav_right + sample_out_offset + i * OUTPUTS * 4);
for (j = 0; j < OUTPUTS; j++) {
mask = speakers_output[j];
for (k = 0; k < 14; k++) {
if (speakers_hesuvi_to_left[k] == mask) {
frame_out_left[j] = frame_in[k];
}
if (speakers_hesuvi_to_right[k] == mask) {
frame_out_right[j] = frame_in[k];
}
}
}
}
free(input_wav);
f = fopen(argv[2], "wb");
if (!f) {
fprintf(stderr, "Unable to open output file: %s\n", argv[2]);
free(output_wav_right);
free(output_wav_left);
return 1;
}
if (fwrite(output_wav_left, 1, output_size, f) != output_size) {
fprintf(stderr, "Unable to write output file\n");
free(output_wav_right);
free(output_wav_left);
return 1;
}
fclose(f);
free(output_wav_left);
f = fopen(argv[3], "wb");
if (!f) {
fprintf(stderr, "Unable to open output file: %s\n", argv[3]);
free(output_wav_right);
return 1;
}
if (fwrite(output_wav_right, 1, output_size, f) != output_size) {
fprintf(stderr, "Unable to write output file\n");
free(output_wav_right);
fclose(f);
return 1;
}
fclose(f);
free(output_wav_right);
return 0;
}
@MagicD3VIL
Copy link

Thanks for the tool! I just noticed that it does not recognize any of the HeSuVi's HRIRs that does not contain reverb. For example oal_dflt, dh- or sonic-.

@kode54
Copy link
Author

kode54 commented Jan 12, 2021

I need to update it to support those, but those are symmetrical responses, so therefore, they only need to be converted to a single output file, and used with the hrir= parameter.

@MagicD3VIL
Copy link

Again thanks for your work on this. I used your converter and tried out a lot of different HeSuVi HRIRs and I must say that cmss_game sounds absolutely great on 8 channels while gaming.

@Dlanis
Copy link

Dlanis commented Feb 24, 2021

"hesuvi_convert razer_fix.wav razer_fixl.wav razer_fixr.wav" and "hesuvi_convert cmss_ent-.wav cmss_entl-.wav cmss_entr-.wav"
WAV has unexpected format

but "hesuvi_convert cmss_ent.wav cmss_entl.wav cmss_entr.wav" works good
and "hrir_left/hrir_right" are don't work (pulseaudio 13.99.1 system linux mint 20.1)

@MagicD3VIL
Copy link

@DlAniAn I've already reported on this in the comment above. It's because the files with "unexpected format" do not contain reverb. Thus most of them have the minus symbol in the name.

Thanks for the tool! I just noticed that it does not recognize any of the HeSuVi's HRIRs that does not contain reverb. For example oal_dflt, dh- or sonic-.

@Dlanis
Copy link

Dlanis commented Feb 25, 2021

Oh sorry I didn't notice (translate google)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment