flnxi-Wu/lib/audio-parser-browser.ts
WuYongJie 63ec6b7dfb fix: 修复排行榜按钮消失、tag排序和输入框浅色模式问题
1. 排行榜悬浮窗关闭后小按钮消失问题
   - 修复面板居中模式关闭时按钮位置错误
   - 保存并恢复按钮的居中状态

2. 首页tag按钮按素材数量排序
   - 添加按素材数量降序排序逻辑

3. 上传弹窗输入框浅色模式文字不可见
   - 添加 text-slate-900 确保浅色模式下文字可见
   - 添加 placeholder 样式确保占位符正确显示
2025-12-14 21:41:29 +08:00

164 lines
5.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 浏览器端音频解析工具
*
* 在用户选择音频文件后,立即在前端解析音频元数据
* 用于自动填充上传表单字段
*
* @module audio-parser-browser
* @created 2025-12-14
*/
import * as musicMetadata from 'music-metadata-browser'
/**
* 音频元数据
*/
export interface AudioMetadataBrowser {
duration: number // 时长(秒)
durationFormatted: string // 格式化时长 (mm:ss)
format: string // 音频格式
bitrate?: number // 比特率
sampleRate?: number // 采样率
channels?: number // 声道数
title?: string // 标题(内嵌标签)
artist?: string // 艺术家(内嵌标签)
album?: string // 专辑(内嵌标签)
year?: number // 年份(内嵌标签)
genre?: string[] // 流派(内嵌标签)
coverImage?: string // 内嵌封面图(base64 data URL)
}
/**
* 检测字符串是否看起来像乱码(GBK被错误解码为ISO-8859-1)
* 乱码特征:包含大量高位字符(0x80-0xFF范围)
*/
function looksLikeGarbledText(str: string | undefined): boolean {
if (!str) return false
// 统计高位字符(Latin-1 扩展字符)的比例
let highByteCount = 0
for (let i = 0; i < str.length; i++) {
const code = str.charCodeAt(i)
if (code >= 0x80 && code <= 0xFF) {
highByteCount++
}
}
// 如果超过30%的字符是高位字符,很可能是乱码
return highByteCount / str.length > 0.3
}
/**
* 尝试将被错误解码的字符串(ISO-8859-1)转换回正确的中文(GBK)
* 原理:ISO-8859-1 是单字节编码,GBK 是双字节编码
* 当 GBK 编码的字节被当作 ISO-8859-1 解码时,每个字节变成一个字符
* 我们需要将这些字符转回字节,然后用 GBK 重新解码
*/
async function tryDecodeAsGBK(str: string | undefined): Promise<string | undefined> {
if (!str || !looksLikeGarbledText(str)) return str
try {
// 将字符串转换回字节数组(假设是 ISO-8859-1 编码)
const bytes = new Uint8Array(str.length)
for (let i = 0; i < str.length; i++) {
bytes[i] = str.charCodeAt(i) & 0xFF
}
// 使用 TextDecoder 以 GBK 编码解码
const decoder = new TextDecoder('gbk')
const decoded = decoder.decode(bytes)
// 验证解码结果是否合理(应该包含中文字符)
const hasChinese = /[\u4e00-\u9fff]/.test(decoded)
if (hasChinese) {
console.log('[AudioParserBrowser] Successfully decoded GBK text:', { original: str, decoded })
return decoded
}
return str
} catch (error) {
console.warn('[AudioParserBrowser] Failed to decode as GBK:', error)
return str
}
}
/**
* 格式化时长为 mm:ss 格式
*/
export function formatDuration(seconds: number): string {
const mins = Math.floor(seconds / 60)
const secs = Math.floor(seconds % 60)
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`
}
/**
* 检查文件是否为音频文件
*/
export function isAudioFile(file: File): boolean {
if (file.type && file.type.startsWith('audio/')) {
return true
}
const ext = file.name.split('.').pop()?.toLowerCase() || ''
const audioExtensions = ['mp3', 'wav', 'flac', 'aac', 'm4a', 'ogg', 'wma', 'aiff', 'ape']
return audioExtensions.includes(ext)
}
/**
* 支持的音频格式
*/
export const AUDIO_ACCEPT_TYPES = 'audio/mpeg,audio/wav,audio/flac,audio/aac,audio/mp4,audio/ogg,audio/webm,.mp3,.wav,.flac,.aac,.m4a,.ogg,.wma'
export const AUDIO_FILE_EXTENSIONS = ['mp3', 'wav', 'flac', 'aac', 'm4a', 'ogg', 'wma', 'aiff', 'ape', 'webm']
/**
* 在浏览器端解析音频文件获取元数据
*
* @param file - 音频文件
* @returns 音频元数据
*/
export async function parseAudioFile(file: File): Promise<AudioMetadataBrowser | null> {
try {
const metadata = await musicMetadata.parseBlob(file)
const duration = metadata.format.duration || 0
// 提取内嵌封面图
let coverImage: string | undefined
if (metadata.common.picture && metadata.common.picture.length > 0) {
const picture = metadata.common.picture[0]
const base64 = btoa(
new Uint8Array(picture.data).reduce((data, byte) => data + String.fromCharCode(byte), '')
)
coverImage = `data:${picture.format};base64,${base64}`
}
// 尝试修复可能的 GBK 编码乱码问题
// 中文 Windows 下的 MP3 文件 ID3 标签常使用 GBK 编码
const title = await tryDecodeAsGBK(metadata.common.title)
const artist = await tryDecodeAsGBK(metadata.common.artist)
const album = await tryDecodeAsGBK(metadata.common.album)
// 处理流派(可能是数组)
let genre = metadata.common.genre
if (genre && genre.length > 0) {
const decodedGenre = await Promise.all(genre.map(g => tryDecodeAsGBK(g)))
genre = decodedGenre.filter((g): g is string => g !== undefined)
}
return {
duration,
durationFormatted: formatDuration(duration),
format: metadata.format.container || 'unknown',
bitrate: metadata.format.bitrate,
sampleRate: metadata.format.sampleRate,
channels: metadata.format.numberOfChannels,
title,
artist,
album,
year: metadata.common.year,
genre,
coverImage,
}
} catch (error) {
console.error('[AudioParserBrowser] Failed to parse audio file:', error)
return null
}
}