HarmonyOS Tutorial: Audio-Driven Wave Ripple Animation with Microphone Input
This HarmonyOS tutorial walks through building a sound-responsive wave ripple animation by capturing microphone amplitude via AudioCapturer, mapping the 0–1 level to a 80–300 pixel radius using a log10 sensitivity curve, animating three staggered circles with opacity fade, and throttling updates to prevent jank.
Permission Setup
The component requires microphone access. Add ohos.permission.MICROPHONE to requestPermissions in module.json with reason and usedScene, then request it at runtime via AtManager.requestPermissionsFromUser. Only after grant (authResult === 0) proceed to audio capture.
Audio Capture Configuration
Create an AudioCapturer with the following stream info:
let audioStreamInfo: audio.AudioStreamInfo = {
channels: audio.AudioChannel.CHANNEL_1,
samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_48000,
sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE,
encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW
};
let audioCapturerInfo: audio.AudioCapturerInfo = {
capturerFlags: 0,
source: audio.SourceType.SOURCE_TYPE_MIC
};
let audioCapturerOptions: audio.AudioCapturerOptions = {
streamInfo: audioStreamInfo,
capturerInfo: audioCapturerInfo
};
this.capturer = await audio.createAudioCapturer(audioCapturerOptions);Listen for readData events and call getMaxAmplitude on each callback.
Reading Maximum Amplitude
Use AudioVolumeGroupManager.getMaxAmplitudeForInputDevice with the current input device descriptor. The returned value ranges from 0 to 1, where higher means louder.
let audioManager = audio.getAudioManager();
let audioVolumeManager: audio.AudioVolumeManager = audioManager.getVolumeManager();
let groupId: number = audio.DEFAULT_VOLUME_GROUP_ID;
let audioVolumeGroupManager: audio.AudioVolumeGroupManager =
await audioVolumeManager.getVolumeGroupManager(groupId);
let deviceDescriptors: audio.AudioDeviceDescriptors | undefined =
this.capturer?.getCurrentInputDevices();
if (deviceDescriptors === undefined) return;
audioVolumeGroupManager.getMaxAmplitudeForInputDevice(deviceDescriptors[0])
.then((value) => {
console.info(`max amplitude is: ${value}`);
})
.catch((err: BusinessError) => {
console.error(`getMaxAmplitudeForInputDevice error. Code: ${err.code}, message: ${err.message}`);
});UI Animation Structure
The visual consists of a centered microphone image (white cutout on colored background) inside a 60×60 circular container, overlaid with three expanding rings. Each ring is a Row with transparent background, a 2px solid border of waveColor, border-radius 50%, and bound to state variables widthSizeN and alphaN.
@State private widthSize1: number = 60;
@State private alpha1: number = 0.8
Row()
.width(this.widthSize1)
.height(this.widthSize1)
.backgroundColor(Color.Transparent)
.border({
color: this.waveColor,
width: 2,
style: BorderStyle.Solid,
radius: this.widthSize1 / 2
})
.opacity(this.alpha1)Animation Loop
Each ring runs an 800ms animateTo with Curve.EaseOut, expanding from 60 to dbMax (the mapped radius) while fading opacity from 0.8 to 0. On finish, reset to initial values. Three rings are staggered by 150ms using setTimeout, triggered only when dbMax >= 80 + sizeMax/10.
this.getUIContext().animateTo({
duration: 800,
curve: Curve.EaseOut,
iterations: 1,
playMode: PlayMode.Normal,
onFinish: () => {
console.info('play end');
this.widthSize1 = 60;
this.alpha1 = 0.8
}
}, () => {
this.widthSize1 = this.dbMax;
this.alpha1 = 0
})
if (this.dbMax >= 80 + this.sizeMax / 10) {
setTimeout(() => {
// start second ring animation
}, 150)
}Amplitude-to-Radius Mapping
Raw amplitude (0–1) is non-linear; human hearing centers around 40–80 dB. A direct linear map would make normal speech barely visible. The solution uses a logarithmic curve with sensitivity coefficient k=99 and a noise threshold of 0.02:
mapLevelToAnimation(rawLevel: number, visualMax: number = this.sizeMax, k: number = 99) {
let noiseThreshold = 0.02
let minNoise = 80
if (rawLevel < 0) rawLevel = 0;
if (rawLevel > 1) rawLevel = 1;
if (rawLevel <= noiseThreshold) return minNoise;
const normalizedValidLevel = (rawLevel - noiseThreshold) / (1 - noiseThreshold);
const logNumerator = Math.log10(1 + k * normalizedValidLevel);
const logDenominator = Math.log10(1 + k);
const mappedRangeValue = (visualMax - minNoise) * (logNumerator / logDenominator);
return minNoise + mappedRangeValue;
}This maps the useful amplitude range to a radius between 80 and visualMax (default 300), suppressing background noise and emphasizing perceptible volume changes.
Animation Throttling
To avoid overlapping animations and performance drain, a minimum interval minInterval is enforced. On each amplitude update, compare current timestamp with lastAnimateTime; if the difference is less than minInterval, skip the frame.
const currentTime = new Date().getTime();
if (currentTime - this.lastAnimateTime < this.minInterval) {
return;
}
this.lastAnimateTime = currentTime;
this.dbMax = this.mapLevelToAnimation(db, this.sizeMax);
this.startAnimation();This ensures each animation completes before the next begins, maintaining smooth visual output.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
51CTO HarmonyOS Developer Community
The HarmonyOS Developer Community is a learning-oriented community for developers to learn, communicate, ask questions, and share.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
