Files

265 lines
6.0 KiB
Vue
Raw Permalink Normal View History

<template>
<div class="audio-encoding-demo">
<div class="demo-header">
<span class="demo-title">声音是如何变成数字的</span>
<span class="demo-subtitle">拖拽滑块调整采样率</span>
</div>
<div class="controls-panel">
<div class="slider-group">
<label>采样频率{{ sampleRate }} /</label>
<input
v-model="sliderValue"
2026-02-24 00:18:09 +08:00
type="range"
min="1"
max="50"
step="1"
class="range-slider"
>
<div class="scale-marks">
<span>低音质 (严重失真)</span>
<span>高音质 (贴近原声)</span>
</div>
</div>
</div>
<div class="wave-visualization">
<!-- Continuous Wave Shape (Analog) -->
<svg class="analog-wave" viewBox="0 0 500 100" preserveAspectRatio="none">
<path :d="analogPath" fill="none" stroke="var(--vp-c-divider)" stroke-width="2" stroke-dasharray="4" />
</svg>
<!-- Digital Samples (Bars) -->
<div class="digital-samples">
<div
v-for="(sample, i) in samples"
:key="i"
class="sample-bar"
:style="{
left: `${sample.x}%`,
height: `${Math.abs(sample.y)}%`,
bottom: sample.y >= 0 ? '50%' : 'auto',
top: sample.y < 0 ? '50%' : 'auto',
width: `${100 / sampleRate}%`
}"
>
<div class="sample-dot" :class="{ 'positive': sample.y >= 0, 'negative': sample.y < 0 }"></div>
</div>
</div>
</div>
<div class="data-stream">
<div class="stream-label">转译后的数字(高度)</div>
<div class="stream-numbers">
<span v-for="(s, i) in displayedNumbers" :key="i" class="num">{{ s }}</span>
<span v-if="samples.length > 15" class="num">...</span>
</div>
</div>
<div class="demo-insight">
说明灰色的虚线是真实的连贯声波大自然的模拟信号蓝色柱子是我们每隔一段时间去测量它的高度数字信号采样频率越密集记录下来的数字就越多恢复出来的声音就越清晰逼真但产生的文件也随之飙升
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const sliderValue = ref(8)
const sampleRate = computed(() => Number(sliderValue.value))
// Generate a smooth sine wave path for the SVG
const analogPath = computed(() => {
let path = 'M 0 50 '
for (let x = 0; x <= 500; x += 5) {
// Generate a compound wave
const normalizedX = x / 500
const y = Math.sin(normalizedX * Math.PI * 4) * 35 + Math.sin(normalizedX * Math.PI * 8) * 10
path += `L ${x} ${50 - y} `
}
return path
})
// Generate discrete samples
const samples = computed(() => {
const result = []
const count = sampleRate.value
for (let i = 0; i <= count; i++) {
const normalizedX = i / count
// Same compound wave formula
const rawY = Math.sin(normalizedX * Math.PI * 4) * 35 + Math.sin(normalizedX * Math.PI * 8) * 10
// Map to percentage of height (0 to 50 for max amplitude)
result.push({
x: normalizedX * 100,
y: rawY, // -45 to +45 roughly
val: Math.round(rawY * 1.5) // scaled value for display
})
}
return result
})
const displayedNumbers = computed(() => {
return samples.value.slice(0, 15).map(s => s.val)
})
</script>
<style scoped>
.audio-encoding-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1.25rem;
margin: 1rem 0;
display: flex;
flex-direction: column;
gap: 1.2rem;
}
.demo-header {
display: flex;
align-items: baseline;
gap: 0.5rem;
}
.demo-title {
font-weight: bold;
font-size: 1rem;
}
.demo-subtitle {
font-size: 0.8rem;
color: var(--vp-c-text-2);
}
.controls-panel {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 1rem;
}
.slider-group label {
display: block;
font-size: 0.9rem;
font-weight: bold;
margin-bottom: 0.8rem;
}
.range-slider {
width: 100%;
accent-color: var(--vp-c-brand);
cursor: pointer;
}
.scale-marks {
display: flex;
justify-content: space-between;
font-size: 0.75rem;
color: var(--vp-c-text-3);
margin-top: 0.5rem;
}
.wave-visualization {
position: relative;
height: 140px;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
overflow: hidden;
}
.analog-wave {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.digital-samples {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.sample-bar {
position: absolute;
background: rgba(59, 130, 246, 0.2);
border-left: 1px solid rgba(59, 130, 246, 0.4);
border-right: 1px solid rgba(59, 130, 246, 0.4);
transform: translateX(-50%);
transition: all 0.2s ease-out;
}
.sample-bar:hover {
background: rgba(59, 130, 246, 0.5);
}
.sample-dot {
position: absolute;
width: 6px;
height: 6px;
background: var(--vp-c-brand);
border-radius: 50%;
left: 50%;
transform: translateX(-50%);
}
.sample-dot.positive { top: -3px; }
.sample-dot.negative { bottom: -3px; }
/* Add center line */
.wave-visualization::after {
content: '';
position: absolute;
top: 50%;
left: 0;
right: 0;
height: 1px;
background: var(--vp-c-divider);
opacity: 0.5;
}
.data-stream {
display: flex;
align-items: center;
gap: 1rem;
background: var(--vp-c-bg-alt);
padding: 0.75rem 1rem;
border-radius: 6px;
}
.stream-label {
font-size: 0.85rem;
font-weight: bold;
color: var(--vp-c-text-2);
white-space: nowrap;
}
.stream-numbers {
display: flex;
flex-wrap: wrap;
gap: 6px;
font-family: monospace;
font-size: 0.85rem;
color: var(--vp-c-brand);
}
.num {
background: var(--vp-c-bg);
padding: 2px 6px;
border-radius: 4px;
border: 1px solid var(--vp-c-divider);
}
.demo-insight {
font-size: 0.85rem;
color: var(--vp-c-text-2);
line-height: 1.6;
border-left: 3px solid var(--vp-c-divider);
padding-left: 0.8rem;
}
</style>