小程序使用音频
微信小程序提供了强大的音频处理能力,包括音频播放、录制、控制等功能。主要涉及以下核心 API:
主要音频 API:
wx.chooseMessageFile - 选择音频文件
wx.createInnerAudioContext - 创建音频上下文
wx.getBackgroundAudioManager - 后台音频管理
wx.recorderManager - 音频录制管理
wx.saveFile - 保存音频文件
音频组件特性:
支持多种音频格式(mp3, aac, wav, ogg 等)
支持播放控制(播放/暂停、进度、音量等)
支持后台播放
支持音频可视化
支持音频录制和编辑
这是一个 JS 文件示例:
// pages/audio/audio.js
Page({
data: {
// 音频列表
audioList: [],
// 当前播放的音频索引
currentAudioIndex: -1,
// 音频上下文
audioContext: null,
// 播放状态
isPlaying: false,
// 播放进度
currentTime: 0,
// 音频总时长
duration: 0,
// 播放进度百分比
progress: 0,
// 音量 (0-1)
volume: 0.7,
// 播放速率 (0.5-2.0)
playbackRate: 1.0,
// 是否循环播放
isLooping: false,
// 是否静音
isMuted: false,
// 加载状态
isLoading: false,
// 错误信息
errorMsg: '',
// 音频信息
audioInfo: {
name: '',
size: 0,
duration: 0,
format: ''
},
// 播放模式 (顺序/随机/单曲循环)
playMode: 'sequential', // sequential, random, single
// 播放历史
playHistory: [],
// 音频频谱数据(用于可视化)
audioSpectrum: [],
// 是否显示音频可视化
showVisualization: false
},
onLoad() {
console.log('音频页面加载');
this.initAudioContext();
this.loadAudioListFromStorage();
},
onUnload() {
// 页面卸载时停止播放
if (this.data.audioContext) {
this.data.audioContext.stop();
this.data.audioContext.destroy();
}
},
onHide() {
// 页面隐藏时暂停播放
if (this.data.isPlaying) {
this.pauseAudio();
}
},
onShow() {
// 页面显示时恢复播放状态
this.updatePlaybackStatus();
},
/**
* 初始化音频上下文
*/
initAudioContext() {
const audioContext = wx.createInnerAudioContext();
audioContext.obeyMuteSwitch = false; // 不受静音键影响
// 设置事件监听
audioContext.onPlay(() => {
console.log('音频开始播放');
this.setData({
isPlaying: true,
errorMsg: ''
});
});
audioContext.onPause(() => {
console.log('音频暂停');
this.setData({
isPlaying: false
});
});
audioContext.onStop(() => {
console.log('音频停止');
this.setData({
isPlaying: false,
currentTime: 0,
progress: 0
});
});
audioContext.onEnded(() => {
console.log('音频播放结束');
this.setData({
isPlaying: false,
currentTime: 0,
progress: 0
});
this.playNextAudio();
});
audioContext.onTimeUpdate(() => {
const currentTime = audioContext.currentTime;
const duration = audioContext.duration;
const progress = duration > 0 ? (currentTime / duration * 100) : 0;
this.setData({
currentTime: currentTime,
duration: duration,
progress: progress
});
// 更新音频可视化数据
this.updateAudioVisualization();
});
audioContext.onWaiting(() => {
console.log('音频加载中');
this.setData({
isLoading: true
});
});
audioContext.onCanPlay(() => {
console.log('音频可以播放');
this.setData({
isLoading: false
});
});
audioContext.onError((res) => {
console.error('音频播放错误', res);
this.handleAudioError(res);
});
audioContext.onSeeked(() => {
console.log('音频跳转完成');
});
this.setData({
audioContext: audioContext
});
},
/**
* 从本地存储加载音频列表
*/
loadAudioListFromStorage() {
try {
const audioList = wx.getStorageSync('audioList') || [];
this.setData({
audioList: audioList
});
console.log('音频列表加载成功', audioList.length);
} catch (error) {
console.error('加载音频列表失败', error);
}
},
/**
* 保存音频列表到本地存储
*/
saveAudioListToStorage() {
try {
wx.setStorageSync('audioList', this.data.audioList);
console.log('音频列表保存成功');
} catch (error) {
console.error('保存音频列表失败', error);
}
},
/**
* 选择音频文件
*/
chooseAudioFile() {
wx.chooseMessageFile({
count: 5, // 最多选择5个
type: 'file',
extension: ['mp3', 'aac', 'wav', 'm4a', 'ogg'], // 支持的音频格式
success: (res) => {
console.log('选择音频文件成功', res);
const tempFiles = res.tempFiles;
const newAudioList = tempFiles.map((file, index) => ({
tempFilePath: file.path,
name: file.name,
size: file.size,
duration: 0, // 需要后续获取
format: this.getFileFormat(file.name),
id: Date.now() + index,
createTime: new Date().toLocaleString(),
isLocal: false
}));
// 获取音频时长信息
this.getAudioDurations(newAudioList).then(audiosWithDuration => {
this.setData({
audioList: [...this.data.audioList, ...audiosWithDuration],
errorMsg: ''
});
this.saveAudioListToStorage();
wx.showToast({
title: `成功选择${newAudioList.length}个音频文件`,
icon: 'success',
duration: 2000
});
});
},
fail: (err) => {
console.error('选择音频文件失败', err);
this.handleAudioError(err, '选择音频文件');
}
});
},
/**
* 获取文件格式
*/
getFileFormat(filename) {
const formats = {
'mp3': 'MP3',
'aac': 'AAC',
'wav': 'WAV',
'm4a': 'M4A',
'ogg': 'OGG',
'flac': 'FLAC'
};
const ext = filename.split('.').pop().toLowerCase();
return formats[ext] || ext.toUpperCase();
},
/**
* 获取音频时长
*/
getAudioDurations(audioList) {
const promises = audioList.map(audio => {
return new Promise((resolve) => {
const tempAudio = wx.createInnerAudioContext();
tempAudio.src = audio.tempFilePath;
tempAudio.onCanPlay(() => {
setTimeout(() => {
const duration = tempAudio.duration || 0;
tempAudio.destroy();
resolve({
...audio,
duration: duration
});
}, 100);
});
tempAudio.onError(() => {
tempAudio.destroy();
resolve({
...audio,
duration: 0
});
});
// 触发加载
tempAudio.play();
setTimeout(() => {
tempAudio.pause();
}, 50);
});
});
return Promise.all(promises);
},
/**
* 录制音频
*/
startRecording() {
if (this.recorderManager) {
this.stopRecording();
}
this.recorderManager = wx.getRecorderManager();
this.recorderManager.onStart(() => {
console.log('开始录音');
this.setData({
isRecording: true,
recordingTime: 0
});
// 开始计时
this.recordingTimer = setInterval(() => {
this.setData({
recordingTime: this.data.recordingTime + 1
});
}, 1000);
});
this.recorderManager.onStop((res) => {
console.log('录音结束', res);
this.setData({
isRecording: false
});
if (this.recordingTimer) {
clearInterval(this.recordingTimer);
}
if (res.tempFilePath) {
this.saveRecordedAudio(res.tempFilePath);
}
});
this.recorderManager.onError((err) => {
console.error('录音错误', err);
this.setData({
isRecording: false,
errorMsg: '录音失败'
});
if (this.recordingTimer) {
clearInterval(this.recordingTimer);
}
});
// 开始录音
this.recorderManager.start({
duration: 60000, // 最长60秒
sampleRate: 44100,
numberOfChannels: 1,
encodeBitRate: 192000,
format: 'aac'
});
},
/**
* 停止录音
*/
stopRecording() {
if (this.recorderManager) {
this.recorderManager.stop();
}
},
/**
* 保存录制的音频
*/
saveRecordedAudio(tempFilePath) {
const newAudio = {
tempFilePath: tempFilePath,
name: `录音_${new Date().toLocaleString()}`,
size: 0,
duration: this.data.recordingTime,
format: 'AAC',
id: Date.now(),
createTime: new Date().toLocaleString(),
isRecorded: true
};
this.setData({
audioList: [newAudio, ...this.data.audioList]
});
this.saveAudioListToStorage();
wx.showToast({
title: '录音保存成功',
icon: 'success'
});
},
/**
* 播放音频
*/
playAudio(index) {
if (index < 0 || index >= this.data.audioList.length) {
wx.showToast({
title: '音频不存在',
icon: 'none'
});
return;
}
const audio = this.data.audioList[index];
if (!audio.tempFilePath) {
wx.showToast({
title: '音频文件无效',
icon: 'none'
});
return;
}
this.setData({
currentAudioIndex: index,
isLoading: true,
errorMsg: ''
});
// 设置音频源
this.data.audioContext.src = audio.tempFilePath;
this.data.audioContext.startTime = 0;
// 应用当前设置
this.data.audioContext.volume = this.data.volume;
this.data.audioContext.playbackRate = this.data.playbackRate;
this.data.audioContext.loop = this.data.isLooping;
// 开始播放
this.data.audioContext.play();
// 更新音频信息
this.setData({
audioInfo: {
name: audio.name,
size: audio.size,
duration: audio.duration,
format: audio.format
}
});
// 添加到播放历史
this.addToPlayHistory(index);
},
/**
* 暂停播放
*/
pauseAudio() {
if (this.data.audioContext) {
this.data.audioContext.pause();
}
},
/**
* 停止播放
*/
stopAudio() {
if (this.data.audioContext) {
this.data.audioContext.stop();
}
},
/**
* 切换播放/暂停
*/
togglePlayPause() {
if (this.data.currentAudioIndex === -1) {
if (this.data.audioList.length > 0) {
this.playAudio(0); // 播放第一个音频
} else {
wx.showToast({
title: '请先选择音频文件',
icon: 'none'
});
}
return;
}
if (this.data.isPlaying) {
this.pauseAudio();
} else {
this.playAudio(this.data.currentAudioIndex);
}
},
/**
* 跳转到指定时间
*/
seekTo(time) {
if (this.data.audioContext) {
this.data.audioContext.seek(time);
this.setData({
currentTime: time
});
}
},
/**
* 调整音量
*/
changeVolume(volume) {
const newVolume = Math.max(0, Math.min(1, volume));
this.setData({
volume: newVolume
});
if (this.data.audioContext) {
this.data.audioContext.volume = newVolume;
}
},
/**
* 调整播放速率
*/
changePlaybackRate(rate) {
const newRate = Math.max(0.5, Math.min(2.0, rate));
this.setData({
playbackRate: newRate
});
if (this.data.audioContext) {
this.data.audioContext.playbackRate = newRate;
}
wx.showToast({
title: `播放速率: ${newRate}x`,
icon: 'none',
duration: 1000
});
},
/**
* 切换循环播放
*/
toggleLoop() {
const newLoop = !this.data.isLooping;
this.setData({
isLooping: newLoop
});
if (this.data.audioContext) {
this.data.audioContext.loop = newLoop;
}
wx.showToast({
title: newLoop ? '循环播放已开启' : '循环播放已关闭',
icon: 'none',
duration: 1000
});
},
/**
* 切换静音
*/
toggleMute() {
const newMuted = !this.data.isMuted;
this.setData({
isMuted: newMuted
});
if (this.data.audioContext) {
this.data.audioContext.volume = newMuted ? 0 : this.data.volume;
}
},
/**
* 播放下一首
*/
playNextAudio() {
if (this.data.audioList.length === 0) return;
let nextIndex;
switch (this.data.playMode) {
case 'sequential':
nextIndex = (this.data.currentAudioIndex + 1) % this.data.audioList.length;
break;
case 'random':
nextIndex = Math.floor(Math.random() * this.data.audioList.length);
break;
case 'single':
nextIndex = this.data.currentAudioIndex;
break;
default:
nextIndex = (this.data.currentAudioIndex + 1) % this.data.audioList.length;
}
this.playAudio(nextIndex);
},
/**
* 播放上一首
*/
playPrevAudio() {
if (this.data.audioList.length === 0) return;
let prevIndex;
if (this.data.currentAudioIndex <= 0) {
prevIndex = this.data.audioList.length - 1;
} else {
prevIndex = this.data.currentAudioIndex - 1;
}
this.playAudio(prevIndex);
},
/**
* 切换播放模式
*/
changePlayMode() {
const modes = ['sequential', 'random', 'single'];
const currentIndex = modes.indexOf(this.data.playMode);
const nextMode = modes[(currentIndex + 1) % modes.length];
this.setData({
playMode: nextMode
});
const modeNames = {
'sequential': '顺序播放',
'random': '随机播放',
'single': '单曲循环'
};
wx.showToast({
title: modeNames[nextMode],
icon: 'none',
duration: 1000
});
},
/**
* 添加到播放历史
*/
addToPlayHistory(index) {
const audio = this.data.audioList[index];
const history = this.data.playHistory.filter(item => item.id !== audio.id);
history.unshift({
id: audio.id,
name: audio.name,
index: index,
playTime: new Date().toLocaleString()
});
// 只保留最近20条记录
if (history.length > 20) {
history.pop();
}
this.setData({
playHistory: history
});
},
/**
* 更新播放状态
*/
updatePlaybackStatus() {
if (this.data.audioContext) {
const audioContext = this.data.audioContext;
this.setData({
isPlaying: !audioContext.paused,
currentTime: audioContext.currentTime,
duration: audioContext.duration,
progress: audioContext.duration > 0 ?
(audioContext.currentTime / audioContext.duration * 100) : 0
});
}
},
/**
* 更新音频可视化
*/
updateAudioVisualization() {
if (!this.data.showVisualization) return;
// 模拟频谱数据(实际项目中需要使用音频分析API)
const spectrum = [];
for (let i = 0; i < 32; i++) {
spectrum.push(Math.random() * 100);
}
this.setData({
audioSpectrum: spectrum
});
},
/**
* 切换音频可视化
*/
toggleVisualization() {
this.setData({
showVisualization: !this.data.showVisualization
});
},
/**
* 处理音频错误
*/
handleAudioError(error, operation = '播放音频') {
let errorMsg = `${operation}失败`;
if (error && error.errMsg) {
switch (error.errMsg) {
case 'MEDIA_ERR_NETWORK':
errorMsg = '网络错误,请检查网络连接';
break;
case 'MEDIA_ERR_DECODE':
errorMsg = '音频解码错误,文件可能已损坏';
break;
case 'MEDIA_ERR_SRC_NOT_SUPPORTED':
errorMsg = '不支持的音频格式';
break;
case 'chooseMessageFile:fail cancel':
errorMsg = '用户取消了选择';
return; // 不显示错误提示
default:
errorMsg = error.errMsg;
}
}
this.setData({
errorMsg: errorMsg,
isLoading: false,
isPlaying: false
});
wx.showToast({
title: errorMsg,
icon: 'none',
duration: 2000
});
},
/**
* 删除音频
*/
deleteAudio(index) {
if (index < 0 || index >= this.data.audioList.length) return;
wx.showModal({
title: '确认删除',
content: '确定要删除这个音频文件吗?',
success: (res) => {
if (res.confirm) {
const audioToDelete = this.data.audioList[index];
const newAudioList = this.data.audioList.filter((_, i) => i !== index);
let newCurrentIndex = this.data.currentAudioIndex;
// 调整当前播放索引
if (index === this.data.currentAudioIndex) {
this.stopAudio();
newCurrentIndex = -1;
} else if (index < this.data.currentAudioIndex) {
newCurrentIndex = this.data.currentAudioIndex - 1;
}
this.setData({
audioList: newAudioList,
currentAudioIndex: newCurrentIndex
});
this.saveAudioListToStorage();
wx.showToast({
title: '删除成功',
icon: 'success'
});
}
}
});
},
/**
* 清空所有音频
*/
clearAllAudios() {
if (this.data.audioList.length === 0) return;
wx.showModal({
title: '确认清空',
content: '确定要清空所有音频文件吗?',
success: (res) => {
if (res.confirm) {
this.setData({
audioList: [],
currentAudioIndex: -1
});
this.stopAudio();
this.saveAudioListToStorage();
wx.showToast({
title: '清空成功',
icon: 'success'
});
}
}
});
},
/**
* 保存音频到本地
*/
saveAudioToLocal(index) {
if (index === undefined) {
index = this.data.currentAudioIndex;
}
if (index === -1) {
wx.showToast({
title: '请先选择音频',
icon: 'none'
});
return;
}
const audio = this.data.audioList[index];
wx.saveFile({
tempFilePath: audio.tempFilePath,
success: (res) => {
console.log('音频保存成功', res);
// 更新音频列表,标记为已保存
const newAudioList = [...this.data.audioList];
newAudioList[index].savedFilePath = res.savedFilePath;
newAudioList[index].isLocal = true;
this.setData({
audioList: newAudioList
});
this.saveAudioListToStorage();
wx.showToast({
title: '保存成功',
icon: 'success'
});
},
fail: (err) => {
console.error('保存音频失败', err);
wx.showToast({
title: '保存失败',
icon: 'none'
});
}
});
},
/**
* 分享音频
*/
onShareAppMessage() {
if (this.data.currentAudioIndex !== -1) {
const currentAudio = this.data.audioList[this.data.currentAudioIndex];
return {
title: `分享音频: ${currentAudio.name}`,
path: '/pages/audio/audio'
};
}
return {
title: '分享我的音频库',
path: '/pages/audio/audio'
};
},
/**
* 格式化时间(秒 -> 分:秒)
*/
formatTime(seconds) {
if (isNaN(seconds)) return '0:00';
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins}:${secs < 10 ? '0' : ''}${secs}`;
},
/**
* 格式化文件大小
*/
formatFileSize(bytes) {
if (!bytes || bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
},
/**
* 创建播放列表
*/
createPlaylist(name, audioIndexes) {
const playlist = {
id: Date.now(),
name: name,
createTime: new Date().toLocaleString(),
audioIndexes: audioIndexes,
count: audioIndexes.length
};
// 保存播放列表到本地存储
try {
const playlists = wx.getStorageSync('playlists') || [];
playlists.push(playlist);
wx.setStorageSync('playlists', playlists);
wx.showToast({
title: '播放列表创建成功',
icon: 'success'
});
} catch (error) {
console.error('保存播放列表失败', error);
}
},
/**
* 导入网络音频
*/
importNetworkAudio(url) {
wx.showLoading({
title: '下载中...',
mask: true
});
wx.downloadFile({
url: url,
success: (res) => {
wx.hideLoading();
if (res.statusCode === 200) {
const newAudio = {
tempFilePath: res.tempFilePath,
name: '网络音频',
size: res.totalBytes,
duration: 0,
format: 'MP3',
id: Date.now(),
createTime: new Date().toLocaleString(),
isFromNetwork: true
};
this.setData({
audioList: [newAudio, ...this.data.audioList]
});
this.saveAudioListToStorage();
wx.showToast({
title: '导入成功',
icon: 'success'
});
}
},
fail: (err) => {
wx.hideLoading();
wx.showToast({
title: '导入失败',
icon: 'none'
});
}
});
},
/**
* 音频搜索
*/
searchAudio(keyword) {
if (!keyword) {
return this.data.audioList;
}
return this.data.audioList.filter(audio =>
audio.name.toLowerCase().includes(keyword.toLowerCase())
);
},
/**
* 获取音频统计信息
*/
getAudioStats() {
const totalCount = this.data.audioList.length;
const totalDuration = this.data.audioList.reduce((sum, audio) =>
sum + (audio.duration || 0), 0
);
const totalSize = this.data.audioList.reduce((sum, audio) =>
sum + (audio.size || 0), 0
);
return {
totalCount,
totalDuration: this.formatTime(totalDuration),
totalSize: this.formatFileSize(totalSize),
averageDuration: this.formatTime(totalDuration / totalCount)
};
}
});
这是一个 WXML 文件示例:
<!-- pages/audio/audio.wxml -->
<view class="container">
<!-- 操作按钮区域 -->
<view class="action-buttons">
<button bindtap="chooseAudioFile">选择音频</button>
<button bindtap="startRecording" wx:if="{{!isRecording}}">开始录音</button>
<button bindtap="stopRecording" wx:if="{{isRecording}}">停止录音</button>
<button bindtap="togglePlayPause">{{isPlaying ? '暂停' : '播放'}}</button>
<button bindtap="stopAudio">停止</button>
<button bindtap="playPrevAudio">上一首</button>
<button bindtap="playNextAudio">下一首</button>
<button bindtap="changePlayMode">播放模式</button>
</view>
<!-- 播放控制区域 -->
<view class="player-controls" wx:if="{{currentAudioIndex !== -1}}">
<view class="audio-info">
<text>正在播放: {{audioInfo.name}}</text>
<text>格式: {{audioInfo.format}} | 时长: {{formatTime(duration)}}</text>
</view>
<view class="progress-control">
<text>{{formatTime(currentTime)}}</text>
<slider
value="{{currentTime}}"
max="{{duration}}"
step="1"
bindchange="seekTo"
activeColor="#007AFF"
/>
<text>{{formatTime(duration)}}</text>
</view>
<view class="control-buttons">
<button bindtap="toggleLoop">{{isLooping ? '关闭循环' : '开启循环'}}</button>
<button bindtap="toggleMute">{{isMuted ? '取消静音' : '静音'}}</button>
<button bindtap="changeVolume(0.5)">音量50%</button>
<button bindtap="changePlaybackRate(1.5)">1.5倍速</button>
</view>
<!-- 音频可视化 -->
<view class="visualization" wx:if="{{showVisualization}}">
<view class="spectrum-bars">
<view
class="spectrum-bar"
wx:for="{{audioSpectrum}}"
wx:key="index"
style="height: {{item}}px"
></view>
</view>
</view>
</view>
<!-- 音频列表 -->
<view class="audio-list">
<view class="audio-item"
wx:for="{{audioList}}"
wx:key="id"
class="{{index === currentAudioIndex ? 'active' : ''}}"
>
<view class="audio-info" bindtap="playAudio" data-index="{{index}}">
<text class="audio-name">{{item.name}}</text>
<text class="audio-details">
{{formatTime(item.duration)}} | {{formatFileSize(item.size)}} | {{item.format}}
</text>
</view>
<view class="audio-actions">
<button bindtap="deleteAudio" data-index="{{index}}">删除</button>
<button bindtap="saveAudioToLocal" data-index="{{index}}">保存</button>
</view>
</view>
</view>
<!-- 状态显示 -->
<view class="status-info">
<text>共 {{audioList.length}} 个音频文件</text>
<text>播放模式: {{playMode === 'sequential' ? '顺序' : playMode === 'random' ? '随机' : '单曲'}}</text>
<text wx:if="{{errorMsg}}">错误: {{errorMsg}}</text>
</view>
<!-- 加载指示器 -->
<view wx:if="{{isLoading}}" class="loading">
<text>加载中...</text>
</view>
</view>
主要功能说明
- 音频选择功能
chooseAudioFile(): 选择音频文件
startRecording(): 开始录音
stopRecording(): 停止录音
支持多种音频格式(mp3, aac, wav, ogg 等)
- 音频播放控制
playAudio(): 播放指定音频
pauseAudio(): 暂停播放
stopAudio(): 停止播放
togglePlayPause(): 播放/暂停切换
seekTo(): 跳转到指定时间
- 播放器功能
changeVolume(): 调整音量
changePlaybackRate(): 调整播放速率
toggleLoop(): 切换循环播放
toggleMute(): 切换静音
changePlayMode(): 切换播放模式
- 播放列表管理
playNextAudio(): 播放下一首
playPrevAudio(): 播放上一首
支持顺序播放、随机播放、单曲循环
播放历史记录
- 音频可视化
updateAudioVisualization(): 更新音频频谱
toggleVisualization(): 显示/隐藏可视化
模拟音频频谱数据(实际项目可使用 Web Audio API)
- 文件管理
deleteAudio(): 删除音频文件
clearAllAudios(): 清空所有音频
saveAudioToLocal(): 保存音频到本地
本地存储管理
- 错误处理
handleAudioError(): 统一错误处理
网络错误、解码错误、格式错误等处理
友好的错误提示
- 工具函数
formatTime(): 时间格式化
formatFileSize(): 文件大小格式化
getAudioStats(): 获取音频统计信息
- 高级功能
音频录制和保存
网络音频导入
播放列表创建
音频搜索功能
后台播放支持
这个示例提供了完整的音频功能实现,包括音频选择、播放控制、文件管理、可视化等功能,可以直接在小程序项目中使用。
通关密语:音频