123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199 |
- <!DOCTYPE html>
- <head>
- <style>
- * {
- font-family: sans-serif;
- }
- pre {
- font-family: monospace;
- }
- a {
- font-family: sans-serif;
- }
- audio {
- width: 100%;
- }
- canvas {
- width: 100%;
- height: 0;
- transition: all linear 0.1s;
- }
- .canvas-active {
- height: 15vh;
- }
- pre {
- overflow: scroll;
- }
- </style>
- </head>
- <body>
- <h1>听歌识曲 Demo (Credit: <a href="https://github.com/mos9527/ncm-afp" target="_blank">https://github.com/mos9527/ncm-afp</a>)</h1>
- <hr>
- <p><b>DISCLAIMER: </b></p>
- <p>This site uses the offical NetEase audio matcher APIs (reverse engineered from <a
- href="https://fn.music.163.com/g/chrome-extension-home-page-beta/">https://fn.music.163.com/g/chrome-extension-home-page-beta/</a>)
- </p>
- <p>And DOES NOT condone copyright infringment nor intellectual property theft.</p>
- <hr>
- <p><b>NOTE:</b></p>
- <p>Before start using the site, you may want to access this link first:</p>
- <a href="https://cors-anywhere.herokuapp.com/corsdemo">https://cors-anywhere.herokuapp.com/corsdemo</a>
- <p>Since Netease APIs do not have CORS headers, this is required to alleviate this restriction.</p>
- <hr>
- <p>Usage:</p>
- <li>Select your audio file through "Choose File" picker</li>
- <li>Hit the "Clip" button and wait for the results!</li>
- <audio id="audio" controls autoplay></audio>
- <canvas id="canvas"></canvas>
- <button id="invoke">Clip</button>
- <input type="file" name="picker" accept="*" id="file">
- <hr>
- <label for="use-mic">Mix in Microphone input</label>
- <input type="checkbox" name="use-mic" id="usemic">
- <hr>
- <pre id="logs"></pre>
- </body>
- <script src="./afp.wasm.js"></script>
- <script src="./afp.js"></script>
- <script type="module">
- const duration = 3
- let audioCtx, recorderNode, micSourceNode
- let audioBuffer, bufferHealth
- let audio = document.getElementById('audio')
- let file = document.getElementById('file')
- let clip = document.getElementById('invoke')
- let usemic = document.getElementById('usemic')
- let canvas = document.getElementById('canvas')
- let canvasCtx = canvas.getContext('2d')
- let logs = document.getElementById('logs')
- logs.write = line => logs.innerHTML += line + '\n'
- function RecorderCallback(channelL) {
- let sampleBuffer = new Float32Array(channelL.subarray(0, duration * 8000))
- GenerateFP(sampleBuffer).then(FP => {
- logs.write(`[index] Generated FP ${FP}`)
- logs.write('[index] Now querying, please wait...')
- fetch(
- '/audio/match?' +
- new URLSearchParams({
- duration: duration, audioFP: FP
- }), {
- method: 'POST'
- }).then(resp => resp.json()).then(resp => {
- if (!resp.data.result) {
- return logs.write('[index] Query failed with no results.')
- }
- logs.write(`[index] Query complete. Results=${resp.data.result.length}`)
- for (var song of resp.data.result) {
- logs.write(
- `[result] <a target="_blank" href="https://music.163.com/song?id=${song.song.id}">${song.song.name} - ${song.song.album.name} (${song.startTime / 1000}s)</a>`
- )
- }
- })
- })
- }
- function InitAudioCtx() {
- // AFP.wasm can't do it with anything other than 8KHz
- audioCtx = new AudioContext({ 'sampleRate': 8000 })
- if (audioCtx.state == 'suspended')
- return false
- let audioNode = audioCtx.createMediaElementSource(audio)
- audioCtx.audioWorklet.addModule('rec.js').then(() => {
- recorderNode = new AudioWorkletNode(audioCtx, 'timed-recorder')
- audioNode.connect(recorderNode) // recorderNode doesn't output anything
- audioNode.connect(audioCtx.destination)
- recorderNode.port.onmessage = event => {
- switch (event.data.message) {
- case 'finished':
- RecorderCallback(event.data.recording)
- clip.innerHTML = 'Clip'
- clip.disabled = false
- canvas.classList.remove('canvas-active')
- break
- case 'bufferhealth':
- clip.innerHTML = `${(duration * (1 - event.data.health)).toFixed(2)}s`
- bufferHealth = event.data.health
- audioBuffer = event.data.recording
- break
- default:
- logs.write(event.data.message)
- }
- }
- // Attempt to get user's microphone and connect it to the AudioContext.
- navigator.mediaDevices.getUserMedia({
- audio: {
- echoCancellation: false,
- autoGainControl: false,
- noiseSuppression: false,
- latency: 0,
- },
- }).then(micStream => {
- micSourceNode = audioCtx.createMediaStreamSource(micStream);
- micSourceNode.connect(recorderNode)
- usemic.checked = true
- logs.write('[rec.js] Microphone attached.')
- });
- });
- return true
- }
- clip.addEventListener('click', event => {
- recorderNode.port.postMessage({
- message: 'start', duration: duration
- })
- clip.disabled = true
- canvas.classList.add('canvas-active')
- })
- usemic.addEventListener('change', event => {
- if (!usemic.checked)
- micSourceNode.disconnect(recorderNode)
- else
- micSourceNode.connect(recorderNode)
- })
- file.addEventListener('change', event => {
- file.files[0].arrayBuffer().then(
- async buffer => {
- logs.write(`[index] File ${file.files[0].name} loaded.`)
- audio.src = window.URL.createObjectURL(new Blob([buffer]))
- clip.disabled = false
- })
- })
- function UpdateCanvas() {
- let w = canvas.clientWidth, h = canvas.clientHeight
- canvas.width = w, canvas.height = h
- canvasCtx.fillStyle = 'rgba(0,0,0,0)';
- canvasCtx.fillRect(0, 0, w, h);
- if (audioBuffer) {
- canvasCtx.fillStyle = 'black';
- for (var x = 0; x < w * bufferHealth; x++) {
- var y = audioBuffer[Math.ceil((x / w) * audioBuffer.length)]
- var z = Math.abs(y) * h / 2
- canvasCtx.fillRect(x, h / 2 - (y > 0 ? z : 0), 1, z)
- }
- }
- requestAnimationFrame(UpdateCanvas)
- }
- UpdateCanvas()
- let requestCtx = setInterval(() => {
- try {
- if (InitAudioCtx()) { // Put this here so we don't have to deal with the 'user did not interact' thing
- clearInterval(requestCtx)
- logs.write('[rec.js] Audio Context started.')
- }
- } catch {
- // Fail silently
- }
- }, 100)
- </script>
|