mirror of
https://github.com/NeteaseCloudMusicApiEnhanced/api-enhanced.git
synced 2026-03-21 11:03:15 +00:00
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
368 lines
9.4 KiB
HTML
368 lines
9.4 KiB
HTML
<!DOCTYPE html>
|
||
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>听歌识曲 Demo</title>
|
||
<style>
|
||
* {
|
||
margin: 0;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
body {
|
||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||
min-height: 100vh;
|
||
background: #f5f5f5;
|
||
padding: 20px;
|
||
}
|
||
|
||
.container {
|
||
max-width: 800px;
|
||
margin: 0 auto;
|
||
background: white;
|
||
border-radius: 12px;
|
||
padding: 32px;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
h1 {
|
||
font-size: 24px;
|
||
font-weight: 600;
|
||
color: #333;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.subtitle {
|
||
font-size: 13px;
|
||
color: #666;
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
hr {
|
||
border: none;
|
||
border-top: 1px solid #eee;
|
||
margin: 20px 0;
|
||
}
|
||
|
||
p {
|
||
font-size: 14px;
|
||
color: #555;
|
||
line-height: 1.6;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
a {
|
||
color: #333;
|
||
text-decoration: none;
|
||
}
|
||
|
||
a:hover {
|
||
text-decoration: underline;
|
||
}
|
||
|
||
.section {
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
.section h3 {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: #333;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.control-group {
|
||
display: flex;
|
||
gap: 12px;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
button {
|
||
padding: 10px 20px;
|
||
background: #333;
|
||
color: white;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
border: none;
|
||
border-radius: 6px;
|
||
cursor: pointer;
|
||
transition: background 0.2s ease;
|
||
}
|
||
|
||
button:hover {
|
||
background: #555;
|
||
}
|
||
|
||
button:disabled {
|
||
background: #999;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
input[type="file"] {
|
||
padding: 10px 14px;
|
||
border: 1px solid #ddd;
|
||
border-radius: 6px;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.checkbox-group {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.checkbox-group input[type="checkbox"] {
|
||
cursor: pointer;
|
||
}
|
||
|
||
.checkbox-group label {
|
||
margin: 0;
|
||
cursor: pointer;
|
||
font-size: 14px;
|
||
color: #555;
|
||
}
|
||
|
||
audio {
|
||
width: 100%;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
canvas {
|
||
width: 100%;
|
||
height: 0;
|
||
transition: all linear 0.1s;
|
||
background: #f9f9f9;
|
||
border-radius: 6px;
|
||
}
|
||
|
||
.canvas-active {
|
||
height: 15vh;
|
||
}
|
||
|
||
pre {
|
||
font-family: 'Courier New', monospace;
|
||
font-size: 13px;
|
||
color: #666;
|
||
white-space: pre-wrap;
|
||
word-wrap: break-word;
|
||
max-height: 400px;
|
||
overflow-y: auto;
|
||
padding: 16px;
|
||
background: #f9f9f9;
|
||
border-radius: 6px;
|
||
border: 1px solid #eee;
|
||
}
|
||
|
||
.warning {
|
||
padding: 12px 16px;
|
||
background: #fef3c7;
|
||
border-radius: 6px;
|
||
font-size: 14px;
|
||
color: #92400e;
|
||
margin-bottom: 16px;
|
||
}
|
||
</style>
|
||
</head>
|
||
|
||
<body>
|
||
<div class="container">
|
||
<h1>听歌识曲 Demo</h1>
|
||
<p class="subtitle">Credit: <a href="https://github.com/mos9527/ncm-afp" target="_blank">https://github.com/mos9527/ncm-afp</a></p>
|
||
|
||
<hr>
|
||
|
||
<div class="warning">
|
||
<strong>免责声明:</strong>本站点使用网易云音乐官方音频识别API(逆向自 <a href="https://fn.music.163.com/g/chrome-extension-home-page-beta/" target="_blank">Chrome 扩展页面</a>),不鼓励版权侵犯或知识产权盗窃。
|
||
</div>
|
||
|
||
<div class="section">
|
||
<h3>使用说明</h3>
|
||
<p>在使用本站点之前,您可能需要先访问以下链接:</p>
|
||
<p><a href="https://cors-anywhere.herokuapp.com/corsdemo" target="_blank">https://cors-anywhere.herokuapp.com/corsdemo</a></p>
|
||
<p>由于网易云音乐API没有CORS头,这是解决此限制的必要步骤。</p>
|
||
</div>
|
||
|
||
<div class="section">
|
||
<h3>使用方法</h3>
|
||
<ul style="padding-left: 20px; font-size: 14px; color: #555;">
|
||
<li>通过"选择文件"选择您的音频文件</li>
|
||
<li>点击"识别"按钮并等待结果</li>
|
||
</ul>
|
||
</div>
|
||
|
||
<hr>
|
||
|
||
<audio id="audio" controls autoplay></audio>
|
||
<canvas id="canvas"></canvas>
|
||
|
||
<div class="control-group">
|
||
<button id="invoke">识别</button>
|
||
<input type="file" name="picker" accept="*" id="file">
|
||
</div>
|
||
|
||
<div class="control-group">
|
||
<div class="checkbox-group">
|
||
<input type="checkbox" name="use-mic" id="usemic">
|
||
<label for="usemic">混合麦克风输入</label>
|
||
</div>
|
||
</div>
|
||
|
||
<hr>
|
||
|
||
<h3 style="font-size: 16px; font-weight: 600; color: #333; margin-bottom: 12px;">日志</h3>
|
||
<pre id="logs"></pre>
|
||
</div>
|
||
</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 => {
|
||
// Append log lines as text to avoid interpreting content as HTML
|
||
logs.appendChild(document.createTextNode(line));
|
||
logs.appendChild(document.createElement('br'));
|
||
}
|
||
|
||
function RecorderCallback(channelL) {
|
||
let sampleBuffer = new Float32Array(channelL.subarray(0, duration * 8000))
|
||
GenerateFP(sampleBuffer).then(FP => {
|
||
logs.write(`[index] 生成指纹 ${FP}`)
|
||
logs.write('[index] 正在查询,请稍候...')
|
||
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] 查询失败,无结果')
|
||
}
|
||
logs.write(`[index] 查询完成。结果数量=${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() {
|
||
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)
|
||
audioNode.connect(audioCtx.destination)
|
||
recorderNode.port.onmessage = event => {
|
||
switch (event.data.message) {
|
||
case 'finished':
|
||
RecorderCallback(event.data.recording)
|
||
clip.innerHTML = '识别'
|
||
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)
|
||
}
|
||
}
|
||
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] 麦克风已连接')
|
||
});
|
||
});
|
||
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)
|
||
})
|
||
function escapeHtml(str) {
|
||
return String(str)
|
||
.replace(/&/g, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"')
|
||
.replace(/'/g, ''')
|
||
.replace(/\//g, '/');
|
||
}
|
||
file.addEventListener('change', event => {
|
||
file.files[0].arrayBuffer().then(
|
||
async buffer => {
|
||
const safeName = escapeHtml(file.files[0].name)
|
||
logs.write(`[index] 文件 ${safeName} 已加载`)
|
||
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()) {
|
||
clearInterval(requestCtx)
|
||
logs.write('[rec.js] 音频上下文已启动')
|
||
}
|
||
} catch {
|
||
// Fail silently
|
||
}
|
||
}, 100)
|
||
</script>
|
||
</html> |