在.NET C#中如何实现WAV音高转调及单周期和弦生成?
让我来一步步帮你解决这两个音频处理的问题,都是C#里很实用的音频开发场景:
1. 在.NET C#中对.wav文件进行音高转调
音高转调的核心是改变音频的频率——频率越高,音高越高。根据对时长的要求,我们有两种实现方式,这里用.NET生态中最常用的音频库NAudio来完成:
简单转调(改变播放速度,时长同步变化)
这种方式适合对时长要求不高的场景,原理是通过修改采样率改变播放速度,间接实现音高变化:
using NAudio.Wave; public void ChangePitchBySpeed(string inputPath, string outputPath, float pitchShiftFactor) { // pitchShiftFactor >1 升高音高,<1 降低音高,比如1.5对应纯五度升高 using var reader = new WaveFileReader(inputPath); // 按音高因子调整采样率 var newSampleRate = (int)(reader.WaveFormat.SampleRate * pitchShiftFactor); var convertedStream = new WaveFormatConversionStream( new WaveFormat(newSampleRate, reader.WaveFormat.BitsPerSample, reader.WaveFormat.Channels), reader); WaveFileWriter.CreateWaveFile(outputPath, convertedStream); }
比如把采样率提高1.5倍,播放时音高会升高纯五度,但时长会缩短为原来的2/3。
变速不变调转调(保持原时长)
如果需要音高变化但时长不变,就得用**WSOLA(波形相似叠加)**这类专业算法,NAudio内置了相关实现:
using NAudio.Wave; using NAudio.Wave.SampleProviders; public void PitchShiftWithoutChangingDuration(string inputPath, string outputPath, int semitones) { // semitones 为正升高,为负降低,比如7对应纯五度升高 using var reader = new WaveFileReader(inputPath); var sampleProvider = reader.ToSampleProvider(); // 计算音高转换因子:2^(半音数/12) float pitchFactor = (float)Math.Pow(2, semitones / 12.0); // WSOLA算法实现变速不变调:音高因子和 tempo 因子乘积为1,保证时长不变 var wsolaProvider = new WsolaSampleProvider(sampleProvider) { PitchFactor = pitchFactor, TempoFactor = 1 / pitchFactor }; // 转换回WaveStream并保存 using var waveProvider = wsolaProvider.ToWaveProvider(); WaveFileWriter.CreateWaveFile(outputPath, waveProvider); }
2. 生成可完美循环的单周期和弦
你说的纯五度案例非常典型,核心是让不同音高的单周期波形循环后的总长度完全一致,这样混合后首尾衔接没有断层,实现完美循环。以下是具体实现步骤:
核心原理
纯五度的频率比是3:2:原始单周期波形的周期为T,转调7个半音后的波形周期为T*(2/3)。原始波形循环2次的总长度是2T,转调后的波形循环3次的总长度是3*(2/3)T=2T,两者长度完全匹配,混合后自然能完美循环。
实现代码(C# + NAudio)
步骤1:加载单周期波形
先读取仅包含一个周期的.wav文件:
using NAudio.Wave; public float[] LoadSingleCycleWave(string wavePath) { using var reader = new WaveFileReader(wavePath); var sampleProvider = reader.ToSampleProvider(); var samples = new float[(int)reader.Length / (reader.WaveFormat.BitsPerSample / 8)]; sampleProvider.Read(samples, 0, samples.Length); return samples; }
步骤2:生成转调后的单周期波形
通过线性插值调整原始波形的样本数,得到对应音高的单周期波形:
public float[] PitchShiftSingleCycle(float[] originalSamples, int semitones) { float pitchFactor = (float)Math.Pow(2, semitones / 12.0); // 新单周期的样本数 = 原始样本数 / 音高因子 int newSampleCount = (int)Math.Round(originalSamples.Length / pitchFactor); var shiftedSamples = new float[newSampleCount]; for (int i = 0; i < newSampleCount; i++) { float originalIndex = i * pitchFactor; int leftIndex = (int)Math.Floor(originalIndex); // 循环取索引,保证单周期的连续性 int rightIndex = (leftIndex + 1) % originalSamples.Length; float fraction = originalIndex - leftIndex; // 线性插值生成新样本 shiftedSamples[i] = originalSamples[leftIndex] * (1 - fraction) + originalSamples[rightIndex] * fraction; } return shiftedSamples; }
步骤3:循环波形并混合
把原始波形循环2次、转调后的波形循环3次,再混合样本并归一化防止爆音:
public float[] GeneratePerfectLoopChord(float[] originalCycle, float[] shiftedCycle) { int totalLength = originalCycle.Length * 2; // 验证转调后的波形循环3次长度是否匹配 if (shiftedCycle.Length * 3 != totalLength) { throw new InvalidOperationException("转调后的波形参数不匹配,无法生成完美循环"); } var mixedSamples = new float[totalLength]; // 填充原始循环波形 for (int i = 0; i < totalLength; i++) { mixedSamples[i] += originalCycle[i % originalCycle.Length]; } // 填充转调后的循环波形 for (int i = 0; i < totalLength; i++) { mixedSamples[i] += shiftedCycle[i % shiftedCycle.Length]; } // 归一化音量 float maxAmplitude = mixedSamples.Max(Math.Abs); if (maxAmplitude > 0) { for (int i = 0; i < totalLength; i++) { mixedSamples[i] /= maxAmplitude; } } return mixedSamples; }
步骤4:保存为可循环的.wav文件
最后把混合后的样本写入文件:
public void SaveWaveFile(float[] samples, string outputPath, WaveFormat waveFormat) { using var writer = new WaveFileWriter(outputPath, waveFormat); foreach (float sample in samples) { writer.WriteSample(sample); } }
调用示例
public void RunChordGenerator() { string singleCyclePath = "single_cycle.wav"; string outputPath = "perfect_loop_power_chord.wav"; // 加载原始单周期波形 float[] originalCycle = LoadSingleCycleWave(singleCyclePath); using var reader = new WaveFileReader(singleCyclePath); var waveFormat = reader.WaveFormat; // 转调7个半音(纯五度) float[] shiftedCycle = PitchShiftSingleCycle(originalCycle, 7); // 生成混合后的完美循环和弦 float[] mixedSamples = GeneratePerfectLoopChord(originalCycle, shiftedCycle); // 保存文件 SaveWaveFile(mixedSamples, outputPath, waveFormat); }
额外提示
- 确保原始单周期波形首尾平滑衔接,否则循环播放会有爆音
- 对于其他和弦(比如大三和弦),可以通过频率比计算最小公倍数来确定每个波形的循环次数。比如大三和弦频率比是4:5:6,原始波形循环6次、3音循环5次、5音循环4次,总长度一致就能完美循环。
内容的提问来源于stack exchange,提问作者Stefan Bookholt
相关产品推荐
相关产品推荐

