减小 .wav 文件的音量会产生严重失真

Decreasing volume of .wav file creates heavy distortion

我有一个令我困惑的问题。我导入一个 .wav 文件并将它们读取为字节。然后我将它们变成整数,然后我将它们全部除以 2(或其他一些数字)以减小音量。然后我制作了一个新的 .wav 文件,我将新数据放入其中。结果是原始音轨失真很大。

滚动到相关 (C#-) 代码的 Main() 方法:

using System;
using System.IO;

namespace ConsoleApp2 {
    class basic {
        public static byte[] bit32(int num) { //turns int into byte array of length 4
            byte[] numbyt = new byte[4] { 0x00, 0x00, 0x00, 0x00 };
            int pow;
            for (int k = 3; k >= 0; k--) {
                pow = (int)Math.Pow(16, 2*k + 1);
                numbyt[k] += (byte)(16*(num/pow));
                num -= numbyt[k]*(pow/16);
                numbyt[k] += (byte)(num/(pow/16));
                num -= (num/(pow/16))*pow/16;
            }
            return numbyt;
        }
        public static byte[] bit16(int num) { //turns int into byte array of length 2
            if (num < 0) {
                num += 65535;
            }
            byte[] numbyt = new byte[2] { 0x00, 0x00 };
            int pow;
            for (int k = 1; k >= 0; k--) {
                pow = (int)Math.Pow(16, 2*k + 1);
                numbyt[k] += (byte)(16*(num/pow));
                num -= numbyt[k]*(pow/16);
                numbyt[k] += (byte)(num/(pow/16));
                num -= (num/(pow/16))*pow/16;
            }
            return numbyt;
        }
        public static int bitint16(byte[] numbyt) { //turns byte array of length 2 into int
            int num = 0;
            num += (int)Math.Pow(16, 2)*numbyt[1];
            num += numbyt[0];
            return num;
        }
    }
    class wavfile: FileStream {
        public wavfile(string name, int len) : base(name, FileMode.Create) {
            int samplerate = 44100;
            byte[] riff = new byte[] { 0x52, 0x49, 0x46, 0x46 };
            this.Write(riff, 0, 4);
            byte[] chunksize;
            chunksize = basic.bit32(36 + len*4);
            this.Write(chunksize, 0, 4);
            byte[] wavebyte = new byte[4] { 0x57, 0x41, 0x56, 0x45 };
            this.Write(wavebyte, 0, 4);
            byte[] fmt = new byte[] { 0x66, 0x6d, 0x74, 0x20 };
            this.Write(fmt, 0, 4);
            byte[] subchunk1size = new byte[] { 0x10, 0x00, 0x00, 0x00 };
            this.Write(subchunk1size, 0, 4);
            byte[] formchann = new byte[] { 0x01, 0x00, 0x02, 0x00 };
            this.Write(formchann, 0, 4);
            byte[] sampleratebyte = basic.bit32(samplerate);
            this.Write(sampleratebyte, 0, 4);
            byte[] byterate = basic.bit32(samplerate*4);
            this.Write(byterate, 0, 4);
            byte[] blockalign = new byte[] { 0x04, 0x00 };
            this.Write(blockalign, 0, 2);
            byte[] bits = new byte[] { 0x10, 0x00 };
            this.Write(bits, 0, 2);
            byte[] data = new byte[] { 0x64, 0x61, 0x74, 0x61 };
            this.Write(data, 0, 4);
            byte[] samplesbyte = basic.bit32(len*4);
            this.Write(samplesbyte, 0, 4);
        }
        public void sound(int[] w, int len, wavfile wavorigin = null) {
            byte[] wavbyt = new byte[len*4];
            for (int t = 0; t < len*2; t++) {
                byte[] wavbit16 = basic.bit16(w[t]);
                wavbyt[2*t] = wavbit16[0];
                wavbyt[2*t + 1] = wavbit16[1];
            }
            this.Write(wavbyt, 0, len*4);
            System.Media.SoundPlayer player = new System.Media.SoundPlayer();
            player.SoundLocation = this.Name;
            while (true) {
                player.Play();
                Console.WriteLine("repeat?");
                if (Console.ReadLine() == "no") {
                    break;
                }
            }
        }
    }
    class Program {
        static void Main() {
            int[] song = new int[45000*2];
            byte[] songbyt = File.ReadAllBytes("name.wav"); //use your stereo, 16bits per sample wav-file
            for (int t = 0; t < 45000*2; t++) {
                byte[] songbytsamp = new byte[2] { songbyt[44 + 2*t], songbyt[44 + 2*t + 1] }; //I skip the header
                song[t] = basic.bitint16(songbytsamp)/2; //I divide by 2 here, remove the "/2" to hear the normal sound again
                //song[t] *= 2;
            }
            wavfile wav = new wavfile("test.wav", 45000); //constructor class that writes the header of a .wav file
            wav.sound(song, 45000); //method that writes the data from "song" into the .wav file
        }
    }
}

问题不在于奇数除以 2 时发生的向下舍入问题;您可以取消注释 song[t] *= 2; 的行,并亲耳听到所有失真再次完全消失。

我一定是在某个地方犯了一个愚蠢的小错误,但我找不到它。我只是想让声音数据更安静,以避免在我添加更多声音时失真。

好吧,我知道这会很愚蠢,我是对的。我忘了说明负数是用带符号的 16 位语言写成 2^15 以上的数字,当你除以 2 时,你将它们推入(非常大的)正值。我修改了我的代码,在除以 2 之前从任何大于 2^15 的数字中减去 2^16。不过我必须感谢这个人:How to reduce volume of wav stream? 如果这意味着我的问题是重复的,那么继续删除它,但我暂时保留它,因为其他人可能会发现它有帮助。

使用Math.Pow 进行位和字节操作是一个非常糟糕的主意。该函数采用 double 值作为输入,returns 为双精度值。它还进行指数运算(不是微不足道的操作)。使用传统的移位和掩码操作更清晰,快得多并且引入噪声的可能性较小(因为双打不准确)。

如您所见,您真的想要使用无符号数量(例如uint/UInt32ushort/UInt16)。做这种工作时,签名扩展会让每个人都感到困惑。

这不是您问题的完整答案,但它确实提供了一种可以说更好的字节操作方法。

首先,创建一个小结构来保存位掩码和移位量的组合:

public struct MaskAndShift {
    public uint Mask {get; set;}
    public int Shift {get; set;}
}

然后我创建了两个这些结构的数组,用于描述应该如何从 uintushort 中提取单个字节。我把它们都放在一个名为 Worker:

的静态 class 中
public static class Worker {
    public static MaskAndShift[] Mask32 = new  MaskAndShift[] {
        new MaskAndShift {Mask = 0xFF000000, Shift = 24},
        new MaskAndShift {Mask = 0x00FF0000, Shift = 16},
        new MaskAndShift {Mask = 0x0000FF00, Shift = 8},
        new MaskAndShift {Mask = 0x000000FF, Shift = 0},
    };
    public static  MaskAndShift[] Mask16 = new  MaskAndShift[] {
        new MaskAndShift {Mask = 0x0000FF00, Shift = 8},
        new MaskAndShift {Mask = 0x000000FF, Shift = 0},
    };
}

查看第一个数组中的第一个条目,它说“要从一个 uint 中提取第一个字节,用 0xFF000000 屏蔽该 uint 并将结果向右移动 24 位” .如果您有字节顺序问题,您可以简单地重新排列数组中的条目。

然后我创建了这个静态函数(在 Worker class 中)将 uint / UInt32 转换为四个字节的数组:

public static byte[] UintToByteArray (uint input) {
    var bytes = new byte[4];
    int i = 0;
    foreach (var maskPair in Mask32) {
        var masked = input & maskPair.Mask;
        if (maskPair.Shift != 0) {
            masked >>= maskPair.Shift;
        }
        bytes[i++] = (byte) masked;
    }
    return bytes;
}

对 16 位 ushort(又名 UInt16)执行相同操作的代码看起来几乎相同(这里可能有机会进行一些重构):

public static byte[] UShortToByteArray (ushort input) {
    var bytes = new byte[2];
    int i = 0;
    foreach (var maskPair in Mask16) {
        var masked = input & maskPair.Mask;
        if (maskPair.Shift != 0) {
            masked >>= maskPair.Shift;
        }
        bytes[i++] = (byte) masked;
    }
    return bytes;
}

反向操作要简单得多(但是,如果您有字节顺序问题,则需要编写代码)。在这里,我只是获取数组的条目,将它们添加到一个值中并移动结果:

public static uint ByteArrayToUint (byte[] bytes) {
    uint result = 0;
    //note that the first time through, result is zero, so shifting is a noop
    foreach (var b in bytes){
        result <<= 8;
        result += b;
    }
    return result;
}

为 16 位版本执行此操作最终实际上是相同的代码,所以...

public static ushort ByteArrayToUshort (byte[] bytes) {
    return (ushort) ByteArrayToUint(bytes); 
}

第一次玩位游戏就不行了。所以我写了一些测试代码:

public static void Main(){
    //pick a nice obvious pattern
    uint bit32Test = (((0xF1u * 0x100u) + 0xE2u) * 0x100u + 0xD3u) * 0x100u + 0xC4u;
    Console.WriteLine("Start");
    Console.WriteLine("Input 32 Value: " + bit32Test.ToString("X"));
    
    var bytes32 = Worker.UintToByteArray(bit32Test);
    foreach (var b in bytes32){
        Console.WriteLine(b.ToString("X"));
    }
    Console.WriteLine();
    
    ushort bit16Test = (ushort)((0xB5u * 0x100u) + 0xA6u);
    Console.WriteLine("Input 16 Value: " + bit16Test.ToString("X"));
    
    var bytes16 = Worker.UShortToByteArray(bit16Test);
    foreach (var b in bytes16){
        Console.WriteLine(b.ToString("X"));
    }

    Console.WriteLine("\r\nNow the reverse");
    
    uint reconstitued32 = Worker.ByteArrayToUint(bytes32);
    Console.WriteLine("Reconstituted 32: " + reconstitued32.ToString("X"));
    
    ushort reconstitued16 = Worker.ByteArrayToUshort(bytes16);
    Console.WriteLine("Reconstituted 16: " + reconstitued16.ToString("X")); 
}

该测试代码的输出如下所示:

Start
Input 32 Value: F1E2D3C4
F1
E2
D3
C4

Input 16 Value: B5A6
B5
A6

Now the reverse
Reconstituted 32: F1E2D3C4
Reconstituted 16: B5A6

另外请注意,我做的所有事情都是十六进制的——这让一切都更容易阅读和理解。