波形文件格式支持名为“smpl”的数据块,大多数 DAW 和大多数采样器插件(如 Kontakt)都可以识别该数据块。数据块允许您为波形文件指定循环开始位置和循环结束位置。
http://www.piclist.com/techref/io/serial/midi/wave.html https://web.archive.org/web/20141226210234/http://www.sonicspot.com/guide/wavefiles.html#smpl
我如何在多个文件上批量获取/设置此数据块的属性?理想情况下,我想做这样的事情:
utility.exe input.wav output.wav --loopstart 44100 --loopend 88200
其中,它以 input.wav 作为输入,并输出 output.wav,其中开始和结束循环点分别插入 1 秒和 2 秒(假设音频为 44.1 kHz)。
我已经了解 SoX、libsndfile 等,但不幸的是它们没有此功能。还有其他方法可以实现吗?
答案1
由于情况非常特殊,我不确定这个解决方案是否最终会对任何人有所帮助,但无论如何我都会发布它以防万一。
我不相信互联网上存在可以设置波浪循环点的命令行实用程序,所以我最终在 Node.js 中编写了一个函数来实现这一点。
module.exports = function()
{
this.SetLoop = function(inputPath, outputPath, start, end)
{
// load input wave
var waveData = require('fs').readFileSync(inputPath);
// get sample rate of wave file
var sampleRateIndex;
for(var i = 0; i < waveData.length-4; i++)
{
if (waveData.toString('utf8', i, i + 4) === 'fmt ') // look for fmt chunk - which contains sample rate among other things - per wave format specification
{
sampleRateIndex = i + 4 + 4 + 2 + 2; // sample rate is 12 bytes after start of 'fmt ' chunk id
break;
}
}
if (sampleRateIndex === undefined)
return;
var sampleRate = waveData.readUInt32LE(sampleRateIndex);
// convert seconds to samples
start = Math.floor(sampleRate*start);
end = Math.floor(sampleRate*end);
// find index (byte offset) of smpl chunk if it exists
var smplIndex;
for(var i = waveData.length-4-1; i >= 0; i--) // start search from end of file going backward, since the smpl chunk is typically written after the actual waveform data
{
if (waveData.toString('utf8', i, i + 4) === 'smpl')
{
smplIndex = i; // start of smpl chunk id
break;
}
}
// if the smpl chunk already exists, remove it
if (smplIndex !== undefined)
{
var smplChunkSize = waveData.readUInt32LE(smplIndex + 4) + 8; // smpl chunk size is specified 4 bytes after start of smpl chunk id. add 8 bytes to include size of smpl chunk header itself
waveData = Buffer.concat( [ waveData.slice(0, smplIndex) , waveData.slice(smplIndex + smplChunkSize) ] ); // splice smpl chunk from wave file data
}
// make new buffer to replace smpl chunk
var smplChunk = Buffer.alloc(68); // the default smpl chunk written here is 60 bytes long. add 8 bytes to include size of smpl chunk header itself
// all bytes other than the ones specified below default to 0 and represent default values for the smpl chunk properties
smplChunk.write('smpl', 0, 4);
smplChunk.writeUInt32LE(60, 4); // the default smpl chunk written here is 60 bytes long
smplChunk.writeUInt32LE(60, 20); // middle C is MIDI note 60, therefore make MIDI unity note 60
smplChunk.writeUInt32LE(1, 36); // write at byte offset 36 that there is one loop cue info in the file
smplChunk.writeUInt32LE(start, 52); // write loop start point at byte offset 52
smplChunk.writeUInt32LE(end, 56); // write loop end point at byte offset 56
// append new smpl chunk to wave file
waveData = Buffer.concat( [ waveData, smplChunk ] );
// change wave file main header data to increase the file size to include smpl chunk (loop points)
var fileSizeIndex;
for(var i = 0; i < waveData.length-4; i++)
{
if (waveData.toString('utf8', i, i + 4) === 'RIFF') // look for RIFF chunk (should always be at the very beginning of file)
{
fileSizeIndex = i + 4; // file size is 4 bytes after start of RIFF chunk id
break;
}
}
if (fileSizeIndex === undefined)
return;
var fileSize = waveData.length-8; // get final length of wave file, minus 8 bytes to not include the RIFF chunk header itself
waveData.writeUInt32LE(fileSize, fileSizeIndex); // write new file length
// write new wave file
require('fs').writeFileSync(outputPath, waveData);
}
}
要使用此函数:
- 安装 Node.js
- 将上述函数保存在文本文件中并重命名为 tools.js
- 打开 Node.js 命令行窗口
require('C:/path/to/where/you/saved/tools.js')();
process.chdir('C:/path/to/wave/files');
SetLoop('input.wav','output.wav',1,2) // set loop start at 1.0 sec and loop end at 2.0 sec
请注意,此代码完全替换了文件的 smpl 块,并因此丢弃了文件中任何现有的提示/循环点。