Как синхронно воспроизводить аудиофайлы в JavaScript?
Я работаю над программой для преобразования текста в азбуку Морзе.
Скажи, что я sos
в sos
Моя программа превратит это в массив [1, 1, 1, 0, 2, 2, 2, 0, 1, 1, 1]
. Где s = dot dot dot
(или 1,1,1
), а o = dash dash dash
(или 2,2,2
). Эта часть довольно проста.
Далее у меня есть два звуковых файла:
var dot = new Audio('dot.mp3');
var dash = new Audio('dash.mp3');
Моя цель - иметь функцию, которая будет воспроизводить dot.mp3
когда он видит 1
, и dash.mp3
когда он видит 2
, и dash.mp3
паузу, когда он видит 0
.
Следующий вид/вид/иногда работает, но я думаю, что это в корне ошибочно, и я не знаю, как это исправить.
function playMorseArr(morseArr) {
for (let i = 0; i < morseArr.length; i++) {
setTimeout(function() {
if (morseArr[i] === 1) {
dot.play();
}
if (morseArr[i] === 2) {
dash.play();
}
}, 250*i);
}
}
Эта проблема:
Я могу перебирать массив и воспроизводить звуковые файлы, но выбор времени - это проблема. Если я не установлю интервал setTimeout()
просто правильно, если последний аудиофайл не будет воспроизведен, и 250ms
истекли, следующий элемент в массиве будет пропущен. Таким образом, dash.mp3
длиннее, чем dot.mp3
. Если мое время слишком короткое, я могу услышать [dot dot dot pause dash dash pause dot dot dot]
или что-то в этом роде.
Эффект, который я хочу
Я хочу, чтобы программа работала так (в псевдокоде):
- посмотреть на
ith
элемент массива - если
1
или 2
, начните воспроизведение звукового файла или создайте паузу - дождитесь окончания звукового файла или сделайте паузу
- увеличьте
i
и вернитесь к шагу 1
Что я думал, но не знаю, как реализовать
Итак, главное, что я хочу, чтобы цикл проходил синхронно. Я использовал обещания в ситуациях, когда у меня было несколько функций, которые я хотел выполнить в определенном порядке, но как мне связать неизвестное число функций?
Я также подумал об использовании пользовательских событий, но у меня та же проблема.
Ответы
Ответ 1
Не используйте HTMLAudioElement для такого рода приложений.
Элементы HTMLMediaElements по своей природе асинхронны, и все, от метода play()
до метода pause()
проходит через очевидную выборку ресурсов и менее очевидную настройку currentTime
- асинхронную.
Это означает, что для приложений, которым требуется идеальное время (например, для чтения азбуки Морзе), эти элементы являются исключительно ненадежными.
Вместо этого используйте Web Audio API и его объекты AudioBufferSourceNode, которыми вы можете управлять с точностью до мкс.
Сначала извлеките все ваши ресурсы как ArrayBuffers, затем при необходимости сгенерируйте и воспроизведите AudioBufferSourceNodes из этих ArrayBuffers.
Вы сможете начать воспроизводить их синхронно или планировать их с большей точностью, чем предложит setTimeout (AudioContext использует свои собственные часы).
Беспокоитесь о влиянии памяти на то, что несколько AudioBufferSourceNodes воспроизводят ваши семплы? Не будь Данные хранятся только один раз в памяти, в AudioBuffer. AudioBufferSourceNodes - это просто просмотр этих данных и они не занимают места.
// I use a lib for Morse encoding, didn't tested it too much though
// https://github.com/Syncthetic/MorseCode/
const morse = Object.create(MorseCode);
const ctx = new (window.AudioContext || window.webkitAudioContext)();
(async function initMorseData() {
// our AudioBuffers objects
const [short, long] = await fetchBuffers();
btn.onclick = e => {
let time = 0; // a simple time counter
const sequence = morse.encode(inp.value);
console.log(sequence); // dots and dashes
sequence.split('').forEach(type => {
if(type === ' ') { // space => 0.5s of silence
time += 0.5;
return;
}
// create an AudioBufferSourceNode
let source = ctx.createBufferSource();
// assign the correct AudioBuffer to it
source.buffer = type === '-' ? long : short;
// connect to our output audio
source.connect(ctx.destination);
// schedule it to start at the end of previous one
source.start(ctx.currentTime + time);
// increment our timer with our sample duration
time += source.buffer.duration;
});
};
// ready to go
btn.disabled = false
})()
.catch(console.error);
function fetchBuffers() {
return Promise.all(
[
'https://dl.dropboxusercontent.com/s/1cdwpm3gca9mlo0/kick.mp3',
'https://dl.dropboxusercontent.com/s/h2j6vm17r07jf03/snare.mp3'
].map(url => fetch(url)
.then(r => r.arrayBuffer())
.then(buf => ctx.decodeAudioData(buf))
)
);
}
<script src="https://cdn.jsdelivr.net/gh/mohayonao/[email protected]0c/build/promise-decode-audio-data.min.js"></script>
<script src="https://cdn.jsdelivr.net/gh/Syncthetic/[email protected]/morsecode.js"></script>
<input type="text" id="inp" value="sos"><button id="btn" disabled>play</button>
Ответ 2
Audio
есть событие ended
которое вы можете прослушивать, поэтому вы можете await
Promise
которое разрешается при срабатывании этого события:
const audios = [undefined, dot, dash];
async function playMorseArr(morseArr) {
for (let i = 0; i < morseArr.length; i++) {
const item = morseArr[i];
await new Promise((resolve) => {
if (item === 0) {
// insert desired number of milliseconds to pause here
setTimeout(resolve, 250);
} else {
audios[item].onended = resolve;
audios[item].play();
}
});
}
}
Ответ 3
Я буду использовать рекурсивный подход, который будет прослушивать аудио закончился событие. Таким образом, каждый раз, когда текущее воспроизведение звука останавливается, метод вызывается снова для воспроизведения следующего.
function playMorseArr(morseArr, idx)
{
// Finish condition.
if (idx >= morseArr.length)
return;
let next = function() {playMorseArr(morseArr, idx + 1)};
if (morseArr[idx] === 1) {
dot.onended = next;
dot.play();
}
else if (morseArr[idx] === 2) {
dash.onended = next;
dash.play();
}
else {
setTimeout(next, 250);
}
}
Вы можете инициализировать процедуру, вызывающую playMorseArr()
с массивом и начальным индексом:
playMorseArr([1, 1, 1, 0, 2, 2, 2, 0, 1, 1, 1], 0);
Тестовый пример (Использование фиктивных mp3
файлов из ответа Kaiido)
let [dot, dash] = [
new Audio('https://dl.dropboxusercontent.com/s/1cdwpm3gca9mlo0/kick.mp3'),
new Audio('https://dl.dropboxusercontent.com/s/h2j6vm17r07jf03/snare.mp3')
];
function playMorseArr(morseArr, idx)
{
// Finish condition.
if (idx >= morseArr.length)
return;
let next = function() {playMorseArr(morseArr, idx + 1)};
if (morseArr[idx] === 1) {
dot.onended = next;
dot.play();
}
else if (morseArr[idx] === 2) {
dash.onended = next;
dash.play();
}
else {
setTimeout(next, 250);
}
}
playMorseArr([1,1,1,0,2,2,2,0,1,1,1], 0);
Ответ 4
Хотя они используются для асинхронных операций, их можно использовать и для синхронных задач. Вы делаете Promise для каждой функции, оборачиваете их в async function
, а затем вызываете их с await
одной за раз. Ниже приведена документация async function
как именованной функции в демонстрационной версии, в реальной демонстрационной версии это стрелочная функция, но в любом случае они совпадают:
/**
* async function sequencer(seq, t)
*
* @param {Array} seq - An array of 0s, 1s, and 2s. Pause. Dot, and Dash respectively.
* @param {Number} t - Number representing the rate in ms.
*/
демонстрация
Примечание: если фрагмент кода не работает, просмотрите Plunker
<!DOCTYPE html>
<html>
<head>
<style>
html,
body {
font: 400 16px/1.5 Consolas;
}
fieldset {
max-width: fit-content;
}
button {
font-size: 18px;
vertical-align: middle;
}
#time {
display: inline-block;
width: 6ch;
font: inherit;
vertical-align: middle;
text-align: center;
}
#morse {
display: inline-block;
width: 30ch;
margin-top: 0px;
font: inherit;
text-align: center;
}
[name=response] {
position: relative;
left: 9999px;
}
</style>
</head>
<body>
<form id='main' action='' method='post' target='response'>
<fieldset>
<legend>Morse Code</legend>
<label>Rate:
<input id='time' type='number' min='300' max='1000' pattern='[2-9][0-9]{2,3}' required value='350'>ms
</label>
<button type='submit'>
🔘➖
</button>
<br>
<label><small>0-Pause, 1-Dot, 2-Dash (no delimiters)</small></label>
<br>
<input id='morse' type='number' min='0' pattern='[012]+' required value='111000222000111'>
</fieldset>
</form>
<iframe name='response'></iframe>
<script>
const dot = new Audio('https://od.lk/s/NzlfOTYzMDgzN18/dot.mp3');
const dash = new Audio('https://od.lk/s/NzlfOTYzMDgzNl8/dash.mp3');
const sequencer = async(array, FW = 350) => {
const pause = () => {
return new Promise(resolve => {
setTimeout(() => resolve(dot.pause(), dash.pause()), FW);
});
}
const playDot = () => {
return new Promise(resolve => {
setTimeout(() => resolve(dot.play()), FW);
});
}
const playDash = () => {
return new Promise(resolve => {
setTimeout(() => resolve(dash.play()), FW + 100);
});
}
for (let seq of array) {
if (seq === 0) {
await pause();
}
if (seq === 1) {
await playDot();
}
if (seq === 2) {
await playDash();
}
}
}
const main = document.forms[0];
const ui = main.elements;
main.addEventListener('submit', function(e) {
let t = ui.time.valueAsNumber;
let m = ui.morse.value;
let seq = m.split('').map(num => Number(num));
sequencer(seq, t);
});
</script>
</body>
</html>