Je travaille sur un programme pour convertir du texte en audio morse.
Disons que je tape sos
. Mon programme transformera cela en tableau [1, 1, 1, 0, 2, 2, 2, 0, 1, 1, 1]
. Où s = dot dot dot
(Ou 1,1,1
) Et o = dash dash dash
(Ou 2,2,2
). Cette partie est assez simple.
Ensuite, j'ai deux fichiers son:
var dot = new Audio('dot.mp3');
var dash = new Audio('dash.mp3');
Mon objectif est d'avoir une fonction qui jouera dot.mp3
Quand elle verra un 1
, Et dash.mp3
Quand elle verra un 2
, Et fera une pause quand elle verra un 0
.
Le type suivant de/genre de/fonctionne parfois, mais je pense qu'il est fondamentalement défectueux et je ne sais pas comment le corriger.
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);
}
}
Le problème:
Je peux parcourir le tableau et lire les fichiers audio, mais le timing est un défi. Si je ne règle pas correctement l'intervalle setTimeout()
, si la lecture du dernier fichier audio n'est pas terminée et que 250ms
S'est écoulé, l'élément suivant du tableau sera ignoré. Donc dash.mp3
Est plus long que dot.mp3
. Si mon timing est trop court, je pourrais entendre [dot dot dot pause dash dash pause dot dot dot]
, Ou quelque chose dans ce sens.
L'effet que je veux
Je veux que le programme se passe comme ça (en pseudocode):
ith
1
ou 2
, lancez la lecture du fichier audio ou créez une pausei
et revenir à étape 1Ce à quoi j'ai pensé, mais je ne sais pas comment l'implémenter
Donc, le cornichon est que je veux que la boucle se déroule de manière synchrone. J'ai utilisé des promesses dans des situations où j'avais plusieurs fonctions que je voulais exécuter dans un ordre spécifique, mais comment chaîner un nombre inconnu de fonctions?
J'ai également envisagé d'utiliser des événements personnalisés, mais j'ai le même problème.
N'utilisez pas HTMLAudioElement pour ce type d'application.
Les HTMLMediaElements sont par nature asynchrones et tout, de la méthode play()
à pause()
et passant par l'extraction de ressources évidente et le paramètre currentTime
moins évident est asynchrone.
Cela signifie que pour les applications qui nécessitent des synchronisations parfaites (comme un lecteur de code Morse), ces éléments sont purement peu fiables.
Utilisez plutôt l'API Web Audio et ses objets AudioBufferSourceNode s, que vous pouvez contrôler avec une précision de µs.
Commencez par récupérer toutes vos ressources en tant que ArrayBuffers, puis si nécessaire, générez et lisez AudioBufferSourceNodes à partir de ces ArrayBuffers.
Vous pourrez commencer à les lire de manière synchrone ou à les planifier avec une précision supérieure à ce que setTimeout vous offrira (AudioContext utilise sa propre horloge).
Vous vous inquiétez de l'impact sur la mémoire de la présence de plusieurs AudioBufferSourceNodes dans vos échantillons? Ne le sois pas. Les données ne sont stockées qu'une seule fois en mémoire, dans l'AudioBuffer. AudioBufferSourceNodes sont juste des vues sur ces données et ne prennent aucune place.
// 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's 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/promise-decode-audio-data@eb4b1322113b08614634559bc12e6a8163b9cf0c/build/promise-decode-audio-data.min.js"></script>
<script src="https://cdn.jsdelivr.net/gh/Syncthetic/MorseCode@master/morsecode.js"></script>
<input type="text" id="inp" value="sos"><button id="btn" disabled>play</button>
Audio
s ont un événement ended
que vous pouvez écouter, vous pouvez donc await
un Promise
qui se résout lorsque cet événement se déclenche:
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();
}
});
}
}
J'utiliserai une approche récursive qui écoutera sur l'événement audio terminé . Ainsi, chaque fois que la lecture audio en cours s'arrête, la méthode est appelée à nouveau pour lire la suivante.
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);
}
}
Vous pouvez initialiser la procédure appelant playMorseArr()
avec le tableau et l'index de démarrage:
playMorseArr([1, 1, 1, 0, 2, 2, 2, 0, 1, 1, 1], 0);
Un exemple de test (Utilisation des fichiers factices mp3
De Réponse de 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);
async
& await
Bien qu'ils soient utilisés pour des opérations asynchrones, ils peuvent également être utilisés pour des tâches synchrones. Vous faites une promesse pour chaque fonction, enveloppez-les dans un async function
, puis appelez-les avec await
un par un. Ce qui suit est la documentation du async function
en tant que fonction nommée dans la démo, celle de la démo réelle est une fonction fléchée mais dans les deux cas, elles sont identiques:
/** * 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. */
Remarque: Si l'extrait de pile ne fonctionne pas, consultez le 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', 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>