从 url 中提取音频片段并使用纯 Web Audio API 播放

Posted

技术标签:

【中文标题】从 url 中提取音频片段并使用纯 Web Audio API 播放【英文标题】:Extracting fragment of audio from a url and play it with pure Web Audio API 【发布时间】:2018-09-26 08:36:41 【问题描述】:

在以下网址:

https://www.tophtml.com/snl/15.mp3

我想在following range 上使用纯Web Audio API 播放一个音频:

range from: second: 306.6
  range to: second: 311.8
     total: 5.2 seconds

我将该文件下载到我的桌​​面(我正在使用Windows 10),然后使用VLC 打开它并获得以下文件信息:

number of channels: 2
       sample rate: 44100 Hz
   bits per sample: 32 (float32)

这里有关于这个概念的信息:

https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API/Basic_concepts_behind_Web_Audio_API#Audio_buffers_frames_samples_and_channels

我从那里得到以下摘录:

我想玩上面评论的range(也贴在这里):

range from: second: 306.6
  range to: second: 311.8
     total: 5.2 seconds

通过从支持请求标头的服务器下载该片段:Range

然后我尝试了以下代码:

...
let num_channels    = 2;
let sample_rate     = 44100;
let range_from      = 0;                                    // Goal: 306.6 seconds
let range_length    = (sample_rate / num_channels) * 5.2;   // Goal:   5.2 seconds
let range_to        = range_from + (range_length - 1);      // "range_to" is inclusive (confirmed)
request.setRequestHeader("Range", "bytes=" + range_from + "-" + range_to);
...

我的问题是:

    我需要为变量找到正确的值:range_from,以便它从第二个开始播放:306.6

    我想知道上面为range_length 指定的值是否正确,因为可能有用于标头的字节等,我的意思是:headers + data

这里有我到目前为止的代码:

window.AudioContext = window.AudioContext || window.webkitAudioContext; // necessary for iPhone (maybe others). Could change a near future.

const URL = 'https://www.tophtml.com/snl/15.mp3';
const context = new AudioContext();

window.addEventListener('load', function() 

	const button_option_1			= document.querySelector('.button_option_1');
	const button_option_1_play		= document.querySelector('.button_option_1_play');
	button_option_1_play.disabled	= true;

	button_option_1.addEventListener('click', async function() 
		let time_start, duration;
		let buffer;
		log('...', false);
		button_option_1_play.disabled = true;
		button_option_1_play.onclick = () => playBuffer(buffer);
		//---
		time_start = new Date().getTime();
		let arrayBuffer = await fetch(URL);
		// download complete
		duration = sprintf('%.2fs', (new Date().getTime()-time_start)/1000);
		log(sprintf('P2. Delay: +%s for download. Wait...', duration));
		//---
		time_start = new Date().getTime();		
		let audioBuffer = await decodeAudioData(context, arrayBuffer);
		// decoding complete
		duration = sprintf('%.2fs', (new Date().getTime()-time_start)/1000);
		log(sprintf('P3. Delay: +%s for decoding.', duration));
		//---
		button_option_1_play.disabled = false;
		buffer = audioBuffer;
		button_option_1_play.click();
	);

);
function playBuffer(buffer, from, duration) 
	const source = context.createBufferSource(); // type of "source": "AudioBufferSourceNode"
	source.buffer = buffer;
	source.connect(context.destination);
	source.start(context.currentTime, from, duration);

function log(text, append = true) 
	let log = document.querySelector('.log');
	if (!append)
		log.innerHTML = '';
	let entry = document.createElement('div');
	entry.innerHTML = text;
	log.appendChild(entry);

function decodeAudioData(context, arrayBuffer) 
	return new Promise(async (resolve, reject) => 
		if (false) 
		else if (context.decodeAudioData.length == 1) 
			// console.log('decodeAudioData / Way 1');
			let audioBuffer = await context.decodeAudioData(arrayBuffer);
			resolve(audioBuffer);
		
		else if (context.decodeAudioData.length == 2) 
			// necessary for iPhone (Safari, Chrome) and Mac (Safari). Could change a near future.
			// console.log('decodeAudioData / Way 2');
			context.decodeAudioData(arrayBuffer, function onSuccess(audioBuffer) 
				resolve(audioBuffer);
			);
		
	);

function fetch(url) 
	return new Promise((resolve, reject) => 
		var request = new XMLHttpRequest();
		request.open('GET', url, true);
		request.responseType = 'arraybuffer';
		let num_channels	= 2;
		let sample_rate		= 44100;
		let range_from		= 0;									// Goal: 306.6 seconds
		let range_length	= (sample_rate / num_channels) * 5.2;	// Goal:   5.2 seconds
		let range_to		= range_from + (range_length - 1);		// "range_to" is inclusive (confirmed)
		request.setRequestHeader("Range", "bytes=" + range_from + "-" + range_to);
		request.onload = function() 
			let arrayBuffer = request.response;
			let byteArray = new Uint8Array(arrayBuffer);
			// console.log(Array.from(byteArray)); // just logging info
			resolve(arrayBuffer);
		
		request.send();
	);
.log 
	display: inline-block;
	font-family: "Courier New", Courier, monospace;
	font-size: 13px;
	margin-top: 10px;
	padding: 4px;	
	background-color: #d4e4ff;

.divider 
	border-top: 1px solid #ccc;
	margin: 10px 0;
<script src="https://cdnjs.cloudflare.com/ajax/libs/sprintf/1.1.1/sprintf.min.js"></script>

<button class="button_option_1">Option 1</button>
<button class="button_option_1_play">Play</button><br />
<div class="log">[empty]</div>

这里有对应的CodePen.io

https://codepen.io/anon/pen/RYXKmP

能否请您为range_from 提供正确的值并将其用于CodePen.io 上的分叉代码?

相关问题:https://engineering.stackexchange.com/questions/23929

[编辑 1]

这里有一个更简单的CodePen.io:https://codepen.io/anon/pen/YJKVde,它专注于检查浏览器在给定随机位置的情况下移动到下一个有效帧的能力。

在我做的一个快速实验中,使用 Windows 10, android, iPhone x Native browser, Chrome, Firefox 的组合,上面的代码只适用于: (Windows 10, Chrome), (Android, Chrome), (Android, Native browser)

很遗憾它不起作用:

 (iPhone, Safari), (iPhone, Chrome), (Windows 10, Firefox), (Android, Firefox) 

有没有办法可以向浏览器开发者提交一个请求来关注这个?

Google ChromeWindows 10Android 上的表现非常好。

如果其他浏览器也这样做会很有趣。

谢谢!

【问题讨论】:

【参考方案1】:

帧长度(秒)= 帧样本数/采样率,即 38.28 帧/秒。

帧长度(字节)= 144*比特率/采样率

所以,您的 fetch() 现在应该可以工作了(我也更改了范围长度):

function fetch(url) 
  return new Promise((resolve, reject) => 
    var request = new XMLHttpRequest();
    request.open('GET', url, true);
    request.responseType = 'arraybuffer';
    let num_channels    = 2;
    let bitrate         = 192000;
    let sample_rate     = 44100;
    let byte_per_sec    = 144 * (bitrate/sample_rate) * 38.28;
    let range_from      = Math.floor(byte_per_sec * 306.6);
    let range_length    = Math.floor(byte_per_sec * 5.2);
    let range_to        = range_from + (range_length - 1);
    request.setRequestHeader("Range", "bytes=" + range_from + "-" + range_to);
    request.onload = function() 
        let arrayBuffer = request.response;
        let byteArray = new Uint8Array(arrayBuffer);
        //******************
            for ( let i = 0; i < byteArray.length; i += 1 ) 
                if (( byteArray[i] === 0b11111111 ) && ( byteArray[ i + 1 ] & 0b11110000 ) === 0b11110000 )
                    log('we have a winner! Frame header at:'+i, true);
                    console.log((parseInt(byteArray[i], 10)).toString(2)); //frame header 4 bytes
                    console.log((parseInt(byteArray[i+1], 10)).toString(2));
                    console.log((parseInt(byteArray[i+2], 10)).toString(2));
                    console.log((parseInt(byteArray[i+3], 10)).toString(2));
                    resolve(arrayBuffer.slice(i));
                    break;
                
            
        //******************
    
    request.send();
  );

编辑 我添加了基本的帧头搜索,我的天,老狐狸也吃它。 对于稳定的解决方案,您必须解析文件头以获取元数据,并将其与帧头数据进行比较。并在找不到标头时执行某些操作... ...

【讨论】:

谢谢@Sven。不幸的是,由于某种原因对我不起作用。它从一开始就在播放。能否请您提供一个分叉的CodePen.io?。应该播放的片段是:"Mister president, why do you keep attacking Amazon?. Do you really hate Jeff Bezos that much?"。谢谢! 是的,范围值不能是浮点数。我修好了。现在它应该可以工作了,这是我听到的片段。 还是不行,我刚试了这段代码:codepen.io/anon/pen/ZMgmxX,得到如下结果:image.ibb.co/f3u2XU/image.png。能否请您提供一个分叉的CodePen.io 与您的更改?谢谢! 以防万一,我在这里开了一个相关的讨论(虽然不是编程):engineering.stackexchange.com/questions/23929 It.does 对我有用:https://ibb.co/e0FwsU 但是!如果它不适合你,那么它是不可捐赠的。 mp3 是帧的集合——我们只是从随机点剪切源文件,并且很可能在帧的中间进行。我的浏览器(或 op.sys 解码器?)修复了第一个损坏的帧,而您的则没有。解决方案是从传入流中搜索帧头并丢弃损坏的帧数据。顺便提一句。由于 mp3 规范,无法计算确切的帧位置。

以上是关于从 url 中提取音频片段并使用纯 Web Audio API 播放的主要内容,如果未能解决你的问题,请参考以下文章

使用ffmpeg从视频文件中提取音频文件视频抽帧和切割视频

从视频文件中提取音频并在 OpenAL 中播放

从浏览器中的麦克风获取音频输入并提取特征

Java中读文件操作

URL的PHP​​和哈希/片段部分

mac怎么把视频中的音频提取出来?