Implementing Voice Recording and Playback with Dynamic Audio Visualization in WeChat Mini Programs
Overview
This implementation demonstrates how to integrate voice recording, playback, and a dynamic audio wave effect into a WeChat Mini Program. It utilizes the RecorderManager for capturing audio and the InnerAudioContext for playback. The UI provides a hold-to-record button and a visualizer that animates during playback.
1. Structure (WXML)
The layout consists of a microphone icon for recording and a container for the playback interface. The playback container includes a dynamic wave indicator composed of bars and a timestamp display.
<view class="audio-container">
<!-- Recording Trigger -->
<image
class="mic-icon"
src="/assets/images/mic.png"
mode="aspectFit"
catchtouchstart="onRecordStart"
catchtouchend="onRecordEnd"
/>
<!-- Playback Interface -->
<view class="player-wrapper" wx:if="{{recordDuration > 0}}" bindtap="onPlayTap">
<view class="wave-indicator">
<view class="bar bar-1"></view>
<view class="bar bar-2" style="opacity: {{visualizer.bar2Opacity}}"></view>
<view class="bar bar-3" style="opacity: {{visualizer.bar3Opacity}}"></view>
</view>
<text class="duration-text">{{recordDuration}}s</text>
</view>
</view>
<!-- Recording Status Overlay -->
<view class="recording-overlay" wx:if="{{isRecording}}">
Recording...
</view>
2. Styling (WXSS)
The styles define the flex layout for the audio controls and the visual appearence of the waveform bars. The overlay is posisioned absolutely to indicate recording status.
.audio-container {
display: flex;
align-items: center;
background-color: #ffffff;
padding: 20rpx;
box-sizing: border-box;
}
.mic-icon {
width: 72rpx;
height: 72rpx;
}
.player-wrapper {
display: flex;
align-items: center;
justify-content: space-between;
margin-left: 32rpx;
width: 160rpx;
height: 68rpx;
background-color: #2b7a7a;
border-radius: 10rpx;
padding: 0 12rpx;
box-sizing: border-box;
}
.wave-indicator {
display: flex;
align-items: center;
height: 100%;
}
.bar {
width: 6rpx;
background-color: #e0e0e0;
margin-left: 8rpx;
}
.bar-1 { height: 12rpx; }
.bar-2 { height: 24rpx; }
.bar-3 { height: 36rpx; }
.duration-text {
font-size: 24rpx;
color: #e0e0e0;
}
.recording-overlay {
position: fixed;
bottom: 300rpx;
left: 50%;
transform: translateX(-50%);
width: 200rpx;
height: 200rpx;
line-height: 200rpx;
background-color: rgba(0, 0, 0, 0.7);
color: white;
text-align: center;
border-radius: 10rpx;
}
3. Logic (JS)
The JavaScript handles the recording liefcycle, processes the audio file, and manages the playback animation loop. The animation loop creates a visual effect by toggling the opacity of the waveform bars.
const audioContext = wx.createInnerAudioContext();
Page({
data: {
isRecording: false,
recordDuration: 0,
visualizer: {
bar2Opacity: 1,
bar3Opacity: 1
},
tempFilePath: ''
},
onLoad() {
this.recorderManager = wx.getRecorderManager();
this.setupRecorderEvents();
// Configure player
audioContext.autoplay = false;
},
setupRecorderEvents() {
this.recorderManager.onStop((res) => {
const { tempFilePath } = res;
const duration = Math.floor(res.duration / 1000);
this.setData({
recordDuration: duration,
tempFilePath: tempFilePath,
isRecording: false
});
// Prepare the player
audioContext.src = tempFilePath;
// Optional: Implement server upload logic here
});
},
onRecordStart() {
this.setData({ isRecording: true });
const options = {
duration: 60000 // Max duration 60s
};
this.recorderManager.start(options);
},
onRecordEnd() {
this.setData({ isRecording: false });
this.recorderManager.stop();
},
onPlayTap() {
if (audioContext.src) {
audioContext.play();
this.startWaveAnimation();
}
},
startWaveAnimation() {
let remainingTime = this.data.recordDuration;
let animInterval;
// Cleanup any existing intervals
if (this.playTimer) clearInterval(this.playTimer);
if (this.waveTimer) clearInterval(this.waveTimer);
const animateFrame = () => {
// Animation Cycle: 1.2 seconds total
this.setData({ visualizer: { bar2Opacity: 0, bar3Opacity: 0 } });
setTimeout(() => {
this.setData({ visualizer: { bar2Opacity: 1, bar3Opacity: 0 } });
}, 300);
setTimeout(() => {
this.setData({ visualizer: { bar2Opacity: 1, bar3Opacity: 1 } });
}, 600);
setTimeout(() => {
this.setData({ visualizer: { bar2Opacity: 0, bar3Opacity: 0 } });
}, 900);
};
// Start animation immediately
animateFrame();
this.waveTimer = setInterval(animateFrame, 1200);
// Countdown timer
this.playTimer = setInterval(() => {
remainingTime--;
if (remainingTime <= 0) {
this.stopAnimation();
}
}, 1000);
// Ensure stop when audio ends naturally
audioContext.onEnded(() => {
this.stopAnimation();
});
},
stopAnimation() {
if (this.playTimer) clearInterval(this.playTimer);
if (this.waveTimer) clearInterval(this.waveTimer);
this.setData({
visualizer: { bar2Opacity: 1, bar3Opacity: 1 }
});
}
});