349 lines
14 KiB
HTML
349 lines
14 KiB
HTML
<!doctype html>
|
|
<html lang="pt-br">
|
|
<head>
|
|
<title>Moan Áudio Link</title>
|
|
<meta charset="UTF-8">
|
|
<link rel="manifest" href="manifest.json">
|
|
<meta name="theme-color" content="#000000">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<link rel="icon" type="image/svg+xml" href="icon-512.png">
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
<link href="https://fonts.googleapis.com/css2?family=Goldman:wght@400;700&family=Josefin+Sans:ital,wght@0,100..700;1,100..700&display=swap" rel="stylesheet">
|
|
<link rel="stylesheet" href="estilo.css">
|
|
</head>
|
|
<body>
|
|
<img src="AudioLinkIcon.svg" alt="Ícone De uma orelha ouvindo um qr code">
|
|
<h1>
|
|
Moan Áudio Link
|
|
</h1>
|
|
|
|
|
|
<div id="capturaSetor" class="setores">
|
|
|
|
<img id="listeningIcon" src="ANIMATEDstreamline--ear-hearing-solid.gif" alt="Ouvindo">
|
|
|
|
<button id="captureStart" class="button-34">Capturar</button>
|
|
<button id="captureStop" class="button-34" hidden>Parar</button>
|
|
|
|
<!-- Radio buttons -->
|
|
<div>
|
|
<label>
|
|
<input type="radio" name="modoCaptura" value="link" checked>
|
|
Link automático
|
|
</label>
|
|
<label style="margin-left: 1em;">
|
|
<input type="radio" name="modoCaptura" value="texto">
|
|
Somente texto
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="redirecionamentoTXT" class="setores">
|
|
<div class="spinner"></div>
|
|
<p>Redirecionando para <span id="rxData"></span> em <span id="countdown">3</span> segundo(s).</p>
|
|
</div>
|
|
|
|
<div id="divTXT" class="setores">
|
|
<p>O código é <span id="rxDataTXT">...</span></p>
|
|
</div>
|
|
|
|
<div id="gerarSetor" class="setores">
|
|
<h2>Gerar código sonoro</h2>
|
|
<textarea name="textarea" id="txData">https://livro.online</textarea><br>
|
|
|
|
<div>
|
|
<button class="button-34" onclick="onSend();">Gerar</button>
|
|
<button class="button-34" onclick="downloadAsMP3()">Download</button>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<p>É uma espécie de "qr code" sonoro. Neste app, você pode gerar um código sonoro a partir de um link e, depois neste mesmo app, ouvir esse código e ir automaticamente para o link.</p>
|
|
|
|
<a class="nav-link" href="https://gitea.livro.online/editoramoan/moan-audio-link">Código fonte</a>
|
|
|
|
|
|
|
|
<script type="text/javascript" src="ggwave.js"></script>
|
|
<script src="https://unpkg.com/lamejs@1.2.0/lame.min.js"></script>
|
|
|
|
<script type='text/javascript'>
|
|
|
|
function getModoCapturaSelecionado() {
|
|
const selecionado = document.querySelector('input[name="modoCaptura"]:checked');
|
|
return selecionado ? selecionado.value : null;
|
|
}
|
|
|
|
|
|
window.AudioContext = window.AudioContext || window.webkitAudioContext;
|
|
window.OfflineAudioContext = window.OfflineAudioContext || window.webkitOfflineAudioContext;
|
|
|
|
var context = null;
|
|
var recorder = null;
|
|
|
|
// the ggwave module instance
|
|
var ggwave = null;
|
|
var parameters = null;
|
|
var instance = null;
|
|
|
|
// instantiate the ggwave instance
|
|
// ggwave_factory comes from the ggwave.js module
|
|
ggwave_factory().then(function(obj) {
|
|
ggwave = obj;
|
|
});
|
|
|
|
var txData = document.getElementById("txData");
|
|
var rxData = document.getElementById("rxData");
|
|
var rxDataTXT = document.getElementById("rxDataTXT");
|
|
var listeningIcon = document.getElementById("listeningIcon");
|
|
var countdown = document.getElementById("countdown");
|
|
var redirecionamentoTXT = document.getElementById("redirecionamentoTXT");
|
|
var capturaSetor = document.getElementById("capturaSetor");
|
|
var captureStop = document.getElementById("captureStop");
|
|
let count = 3; // 3 segundos para o countdown
|
|
const countdownEl = document.getElementById("countdown");
|
|
const radios = document.querySelectorAll('input[name="modoCaptura"]');
|
|
const divTXT = document.getElementById("divTXT");
|
|
|
|
|
|
// helper function
|
|
function convertTypedArray(src, type) {
|
|
var buffer = new ArrayBuffer(src.byteLength);
|
|
var baseView = new src.constructor(buffer).set(src);
|
|
return new type(buffer);
|
|
}
|
|
|
|
// initialize audio context and ggwave
|
|
function init() {
|
|
if (!context) {
|
|
context = new AudioContext({sampleRate: 48000});
|
|
|
|
parameters = ggwave.getDefaultParameters();
|
|
parameters.sampleRateInp = context.sampleRate;
|
|
parameters.sampleRateOut = context.sampleRate;
|
|
instance = ggwave.init(parameters);
|
|
}
|
|
}
|
|
|
|
//Função para atualizar a visibilidade da divTXT
|
|
function atualizarVisibilidadeDivTXT() {
|
|
const selecionado = document.querySelector('input[name="modoCaptura"]:checked');
|
|
if (selecionado?.value === "texto") {
|
|
divTXT.style.display = "FLEX";
|
|
} else {
|
|
divTXT.style.display = "none";
|
|
}
|
|
}
|
|
|
|
// Executa ao mudar os radio buttons
|
|
radios.forEach(radio => {
|
|
radio.addEventListener("change", atualizarVisibilidadeDivTXT);
|
|
});
|
|
|
|
// Executa uma vez ao carregar a página
|
|
document.addEventListener("DOMContentLoaded", atualizarVisibilidadeDivTXT);
|
|
|
|
|
|
//
|
|
// Tx
|
|
//
|
|
|
|
let lastAudioBuffer = null; // variável global
|
|
|
|
function onSend() {
|
|
init();
|
|
|
|
// pause audio capture during transmission
|
|
captureStop.click();
|
|
|
|
// generate audio waveform
|
|
var waveform = ggwave.encode(instance, txData.value, ggwave.ProtocolId.GGWAVE_PROTOCOL_AUDIBLE_FAST, 10)
|
|
|
|
// play audio
|
|
var buf = convertTypedArray(waveform, Float32Array);
|
|
var buffer = context.createBuffer(1, buf.length, context.sampleRate);
|
|
buffer.getChannelData(0).set(buf);
|
|
|
|
// salvar buffer para download posterior
|
|
lastAudioBuffer = buffer;
|
|
|
|
var source = context.createBufferSource();
|
|
source.buffer = buffer;
|
|
source.connect(context.destination);
|
|
source.start(0);
|
|
}
|
|
|
|
//Dar a opção de download
|
|
function downloadAsMP3() {
|
|
if (!lastAudioBuffer) {
|
|
alert("Nenhum áudio gerado ainda.");
|
|
return;
|
|
}
|
|
|
|
const samples = lastAudioBuffer.getChannelData(0);
|
|
const mp3encoder = new lamejs.Mp3Encoder(1, lastAudioBuffer.sampleRate, 128); // mono, sampleRate, 128kbps
|
|
const blockSize = 1152;
|
|
let mp3Data = [];
|
|
|
|
for (let i = 0; i < samples.length; i += blockSize) {
|
|
const sampleChunk = samples.subarray(i, i + blockSize);
|
|
const int16Chunk = floatTo16BitPCM(sampleChunk);
|
|
const mp3buf = mp3encoder.encodeBuffer(int16Chunk);
|
|
if (mp3buf.length > 0) mp3Data.push(mp3buf);
|
|
}
|
|
|
|
const mp3buf = mp3encoder.flush();
|
|
if (mp3buf.length > 0) mp3Data.push(mp3buf);
|
|
|
|
const blob = new Blob(mp3Data, { type: 'audio/mp3' });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement("a");
|
|
a.href = url;
|
|
a.download = "moan-audio-link.mp3";
|
|
a.click();
|
|
URL.revokeObjectURL(url);
|
|
}
|
|
|
|
// utilitário para converter float [-1, 1] para int16
|
|
function floatTo16BitPCM(float32Array) {
|
|
const output = new Int16Array(float32Array.length);
|
|
for (let i = 0; i < float32Array.length; i++) {
|
|
let s = Math.max(-1, Math.min(1, float32Array[i]));
|
|
output[i] = s < 0 ? s * 0x8000 : s * 0x7FFF;
|
|
}
|
|
return output;
|
|
}
|
|
|
|
|
|
|
|
|
|
//
|
|
// Rx
|
|
//
|
|
|
|
captureStart.addEventListener("click", function () {
|
|
init();
|
|
|
|
let constraints = {
|
|
audio: {
|
|
// not sure if these are necessary to have
|
|
echoCancellation: false,
|
|
autoGainControl: false,
|
|
noiseSuppression: false
|
|
}
|
|
};
|
|
|
|
navigator.mediaDevices.getUserMedia(constraints).then(function (e) {
|
|
mediaStream = context.createMediaStreamSource(e);
|
|
|
|
var bufferSize = 1024;
|
|
var numberOfInputChannels = 1;
|
|
var numberOfOutputChannels = 1;
|
|
|
|
if (context.createScriptProcessor) {
|
|
recorder = context.createScriptProcessor(
|
|
bufferSize,
|
|
numberOfInputChannels,
|
|
numberOfOutputChannels);
|
|
} else {
|
|
recorder = context.createJavaScriptNode(
|
|
bufferSize,
|
|
numberOfInputChannels,
|
|
numberOfOutputChannels);
|
|
}
|
|
|
|
recorder.onaudioprocess = function (e) {
|
|
var source = e.inputBuffer;
|
|
var res = ggwave.decode(instance, convertTypedArray(new Float32Array(source.getChannelData(0)), Int8Array));
|
|
var link;
|
|
|
|
if (res && res.length > 0) {
|
|
|
|
if (getModoCapturaSelecionado() == "link") {//Se é link automático
|
|
|
|
res = new TextDecoder("utf-8").decode(res);
|
|
// Remove "http://" ou "https://" do início da string
|
|
res = res.replace(/^https?:\/\//, "");
|
|
|
|
// Adiciona "https://" no início
|
|
res = "https://" + res;
|
|
|
|
// Adiciona tag a
|
|
link = "<a src=\"" + res + "\">" + res + "</a>";
|
|
|
|
// remove o primeiro setor. O setor de captura
|
|
capturaSetor.style.display = "none";
|
|
|
|
// Mostra o redirecionamento
|
|
redirecionamentoTXT.style.display = "flex";
|
|
|
|
// Atribui o valor ao rxData
|
|
rxData.innerHTML = link;
|
|
|
|
//countdown e redirecionamento em 3 segundos
|
|
const interval = setInterval(() => {
|
|
count--;
|
|
countdownEl.textContent = count;
|
|
|
|
if (count <= 0) {
|
|
clearInterval(interval);
|
|
window.location.href = res;
|
|
}
|
|
}, 1000);
|
|
|
|
// Aguarda 3 segundos e redireciona
|
|
setTimeout(() => {
|
|
window.location.href = res;
|
|
}, 3000);
|
|
} else if (getModoCapturaSelecionado() == "texto") { //Se é somente texto
|
|
res = new TextDecoder("utf-8").decode(res);
|
|
|
|
|
|
// Mostra o texto
|
|
divTXT.style.display = "flex";
|
|
rxDataTXT.innerHTML = res;
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
mediaStream.connect(recorder);
|
|
recorder.connect(context.destination);
|
|
}).catch(function (e) {
|
|
console.error(e);
|
|
});
|
|
|
|
listeningIcon.style.visibility = "visible";
|
|
captureStart.hidden = true;
|
|
captureStop.hidden = false;
|
|
});
|
|
|
|
captureStop.addEventListener("click", function () {
|
|
if (recorder) {
|
|
recorder.disconnect(context.destination);
|
|
mediaStream.disconnect(recorder);
|
|
recorder = null;
|
|
}
|
|
|
|
listeningIcon.style.visibility = "hidden";
|
|
captureStart.hidden = false;
|
|
captureStop.hidden = true;
|
|
});
|
|
|
|
captureStop.click();
|
|
</script>
|
|
|
|
<script>
|
|
//Service Worker para funcionar como web app no android
|
|
if ('serviceWorker' in navigator) {
|
|
navigator.serviceWorker.register('service-worker.js')
|
|
.then(reg => console.log('[PWA] Service Worker registrado:', reg.scope))
|
|
.catch(err => console.error('[PWA] Falha no Service Worker:', err));
|
|
}
|
|
</script>
|
|
|
|
|
|
</body>
|
|
</html>
|