鸿蒙跨端离线语音备忘录应用开发指南
·
鸿蒙跨端离线语音备忘录应用开发指南
一、项目概述
本文基于HarmonyOS的音频处理能力和分布式数据同步技术,开发一款离线语音备忘录应用。该应用采用本地语音识别引擎优化、录音过程CPU频率调节和存储空间智能清理策略,并借鉴《鸿蒙跨端U同步》中多设备数据同步的技术原理,实现语音备忘录的高效采集、处理和跨设备同步。
二、系统架构
+---------------------+ +---------------------+ +---------------------+
| 主设备 |<----->| 分布式数据总线 |<----->| 从设备 |
| (手机/平板) | | (Distributed Bus) | | (手表/其他设备) |
+----------+----------+ +----------+----------+ +----------+----------+
| | |
+----------v----------+ +----------v----------+ +----------v----------+
| 语音处理模块 | | 存储管理模块 | | 数据同步模块 |
| (语音识别/录音) | | (本地存储/清理) | | (备忘录同步) |
+---------------------+ +---------------------+ +---------------------+
三、核心代码实现
1. 语音服务实现
// src/main/ets/service/VoiceService.ts
import { distributedData } from '@ohos.data.distributedData';
import { BusinessError } from '@ohos.base';
import { audio } from '@ohos.multimedia.audio';
import { fileIo } from '@ohos.fileio';
import { zlib } from '@ohos.zlib';
import { power } from '@ohos.power';
interface VoiceMemo {
id: string;
timestamp: number;
audioPath: string;
transcript: string;
duration: number;
deviceId: string;
isSynced: boolean;
isDeleted: boolean;
}
export class VoiceService {
private static instance: VoiceService;
private kvStore: distributedData.KVStore | null = null;
private readonly STORE_ID = 'voice_memo_store';
private audioRecorder: audio.AudioRecorder | null = null;
private voiceMemos: VoiceMemo[] = [];
private lastSyncTime: number = 0;
private readonly SYNC_INTERVAL = 30 * 60 * 1000; // 30分钟同步一次
private readonly MAX_STORAGE_MB = 100; // 最大存储空间100MB
private constructor() {
this.initKVStore();
this.loadLocalData();
this.cleanupStorage();
}
public static getInstance(): VoiceService {
if (!VoiceService.instance) {
VoiceService.instance = new VoiceService();
}
return VoiceService.instance;
}
private async initKVStore(): Promise<void> {
try {
const options: distributedData.KVManagerConfig = {
bundleName: 'com.example.voicememo',
userInfo: {
userId: '0',
userType: distributedData.UserType.SAME_USER_ID
}
};
const kvManager = distributedData.createKVManager(options);
this.kvStore = await kvManager.getKVStore({
storeId: this.STORE_ID,
options: {
createIfMissing: true,
encrypt: false,
backup: false,
autoSync: true,
kvStoreType: distributedData.KVStoreType.SINGLE_VERSION
}
});
this.kvStore.on('dataChange', distributedData.SubscribeType.SUBSCRIBE_TYPE_REMOTE, (data) => {
this.handleRemoteDataChange(data);
});
} catch (e) {
console.error(`Failed to initialize KVStore. Code: ${e.code}, message: ${e.message}`);
}
}
private async loadLocalData(): Promise<void> {
try {
const memoFile = await fileIo.open('data/voice_memos.bin', 0o666);
const compressedData = await fileIo.read(memoFile.fd, new ArrayBuffer(0));
await fileIo.close(memoFile.fd);
if (compressedData) {
const decompressed = await zlib.deflateSync(compressedData);
this.voiceMemos = JSON.parse(String.fromCharCode.apply(null, new Uint8Array(decompressed)));
}
} catch (e) {
console.log('No local voice memo data found or error reading file');
}
}
private async saveLocalData(): Promise<void> {
try {
// 确保目录存在
await fileIo.mkdir('data');
// 保存语音备忘录数据
const memoStr = JSON.stringify(this.voiceMemos);
const memoCompressed = await zlib.inflateSync(new Uint8Array(memoStr.split('').map(c => c.charCodeAt(0))));
const memoFile = await fileIo.open('data/voice_memos.bin', 0o666 | fileIo.OpenMode.CREATE);
await fileIo.write(memoFile.fd, memoCompressed.buffer);
await fileIo.close(memoFile.fd);
} catch (e) {
console.error(`Failed to save local data. Code: ${e.code}, message: ${e.message}`);
}
}
private async cleanupStorage(): Promise<void> {
try {
// 计算当前存储使用量
let totalSize = 0;
const activeMemos = this.voiceMemos.filter(m => !m.isDeleted);
for (const memo of activeMemos) {
try {
const stat = await fileIo.stat(memo.audioPath);
totalSize += stat.size;
} catch (e) {
console.log(`Audio file not found: ${memo.audioPath}`);
}
}
// 转换为MB
const totalSizeMB = totalSize / (1024 * 1024);
// 如果超过最大限制,清理旧文件
if (totalSizeMB > this.MAX_STORAGE_MB) {
// 按时间排序,最早的在前
const sortedMemos = [...activeMemos].sort((a, b) => a.timestamp - b.timestamp);
let freedSpace = 0;
for (const memo of sortedMemos) {
if (totalSizeMB - freedSpace <= this.MAX_STORAGE_MB * 0.9) {
break; // 清理到90%容量停止
}
try {
const stat = await fileIo.stat(memo.audioPath);
await fileIo.unlink(memo.audioPath);
freedSpace += stat.size / (1024 * 1024);
memo.isDeleted = true;
} catch (e) {
console.log(`Failed to delete audio file: ${memo.audioPath}`);
}
}
if (freedSpace > 0) {
console.log(`Freed ${freedSpace.toFixed(2)}MB storage space`);
await this.saveLocalData();
await this.syncData();
}
}
} catch (e) {
console.error(`Failed to cleanup storage. Code: ${e.code}, message: ${e.message}`);
}
}
public async startRecording(): Promise<string | null> {
if (this.audioRecorder) {
console.log('Recording is already in progress');
return null;
}
try {
// 优化CPU频率以平衡性能和功耗
await power.enablePowerMode(power.PowerMode.NORMAL, 'Voice recording in progress');
// 创建音频录制器
const audioStreamInfo: audio.AudioStreamInfo = {
samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_16000,
channels: audio.AudioChannel.CHANNEL_1,
sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE,
encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW
};
const audioRecorderConfig: audio.AudioRecorderConfig = {
audioStreamInfo,
uri: 'file://data/audio/record_' + Date.now() + '.wav',
fileFormat: audio.AudioFileFormat.WAV
};
this.audioRecorder = await audio.createAudioRecorder();
await this.audioRecorder.prepare(audioRecorderConfig);
await this.audioRecorder.start();
return audioRecorderConfig.uri;
} catch (e) {
console.error(`Failed to start recording. Code: ${e.code}, message: ${e.message}`);
await power.disablePowerMode(power.PowerMode.NORMAL);
this.audioRecorder = null;
return null;
}
}
public async stopRecording(uri: string): Promise<VoiceMemo | null> {
if (!this.audioRecorder) {
console.log('No recording in progress');
return null;
}
try {
await this.audioRecorder.stop();
await this.audioRecorder.release();
this.audioRecorder = null;
// 恢复默认CPU频率
await power.disablePowerMode(power.PowerMode.NORMAL);
// 获取录音时长
const duration = await this.getAudioDuration(uri);
// 本地语音识别(简化版,实际应使用本地ASR引擎)
const transcript = await this.localSpeechRecognition(uri);
// 创建语音备忘录
const memo: VoiceMemo = {
id: 'memo_' + Date.now(),
timestamp: Date.now(),
audioPath: uri,
transcript: transcript || '(未识别内容)',
duration,
deviceId: this.getDeviceId(),
isSynced: false,
isDeleted: false
};
this.voiceMemos.push(memo);
await this.saveLocalData();
// 异步同步数据
setTimeout(() => {
this.syncData();
}, 1000);
return memo;
} catch (e) {
console.error(`Failed to stop recording. Code: ${e.code}, message: ${e.message}`);
await power.disablePowerMode(power.PowerMode.NORMAL);
this.audioRecorder = null;
return null;
}
}
private async getAudioDuration(uri: string): Promise<number> {
try {
const fileStat = await fileIo.stat(uri.replace('file://', ''));
const fileSize = fileStat.size;
// WAV文件头44字节,16kHz 16bit单声道每秒32000字节
const audioDataSize = fileSize - 44;
return Math.ceil(audioDataSize / 32000);
} catch (e) {
console.error(`Failed to get audio duration. Code: ${e.code}, message: ${e.message}`);
return 0;
}
}
private async localSpeechRecognition(uri: string): Promise<string> {
// 实际应用中应集成本地ASR引擎
// 这里简化为返回固定文本
return "这是语音备忘录的示例文本";
}
private getDeviceId(): string {
// 实际应用中应获取真实设备ID
return 'device_' + Math.random().toString(36).substr(2, 9);
}
private async syncData(): Promise<void> {
if (!this.kvStore) return;
try {
// 同步语音备忘录
const unsyncedMemos = this.voiceMemos.filter(m => !m.isSynced);
if (unsyncedMemos.length > 0) {
await this.kvStore.put('voice_memos', { value: unsyncedMemos });
this.voiceMemos.forEach(m => {
if (!m.isSynced) m.isSynced = true;
});
}
} catch (e) {
console.error(`Failed to sync data. Code: ${e.code}, message: ${e.message}`);
}
}
private handleRemoteDataChange(data: distributedData.ChangeData): void {
data.insertEntries.forEach((entry: distributedData.Entry) => {
if (entry.key === 'voice_memos') {
const remoteMemos = entry.value.value as VoiceMemo[];
this.mergeVoiceMemos(remoteMemos);
}
});
}
private mergeVoiceMemos(remoteMemos: VoiceMemo[]): void {
remoteMemos.forEach(remote => {
const existing = this.voiceMemos.find(local => local.id === remote.id);
if (!existing) {
this.voiceMemos.push(remote);
} else {
// 合并策略:保留最新的转录文本
if (remote.timestamp > existing.timestamp) {
existing.transcript = remote.transcript;
existing.duration = remote.duration;
}
// 同步删除状态
if (remote.isDeleted && !existing.isDeleted) {
existing.isDeleted = true;
// 实际应用中应删除本地文件
}
}
});
// 按时间排序
this.voiceMemos.sort((a, b) => b.timestamp - a.timestamp);
}
public async deleteMemo(memoId: string): Promise<boolean> {
const memo = this.voiceMemos.find(m => m.id === memoId);
if (!memo) return false;
try {
// 标记为已删除
memo.isDeleted = true;
memo.isSynced = false;
// 异步删除音频文件
setTimeout(async () => {
try {
await fileIo.unlink(memo.audioPath.replace('file://', ''));
} catch (e) {
console.log(`Failed to delete audio file: ${memo.audioPath}`);
}
}, 0);
await this.saveLocalData();
await this.syncData();
return true;
} catch (e) {
console.error(`Failed to delete memo. Code: ${e.code}, message: ${e.message}`);
return false;
}
}
public getMemos(): VoiceMemo[] {
return this.voiceMemos.filter(m => !m.isDeleted);
}
public async playMemo(memoId: string): Promise<void> {
const memo = this.voiceMemos.find(m => m.id === memoId);
if (!memo || memo.isDeleted) return;
try {
const audioPlayer = await audio.createAudioPlayer();
await audioPlayer.prepare({
uri: memo.audioPath
});
await audioPlayer.play();
audioPlayer.on('stateChange', (state) => {
if (state === audio.AudioState.STOPPED || state === audio.AudioState.RELEASED) {
audioPlayer.release();
}
});
} catch (e) {
console.error(`Failed to play memo. Code: ${e.code}, message: ${e.message}`);
}
}
public async destroy(): Promise<void> {
if (this.audioRecorder) {
await this.audioRecorder.release();
this.audioRecorder = null;
}
if (this.kvStore) {
this.kvStore.off('dataChange');
}
await this.saveLocalData();
}
}
2. 语音备忘录组件实现
// src/main/ets/components/VoiceMemoList.ets
@Component
export struct VoiceMemoList {
private voiceService = VoiceService.getInstance();
@State memos: VoiceMemo[] = [];
@State isRecording: boolean = false;
@State currentRecordingUri: string | null = null;
private timer: number = 0;
@State recordingDuration: number = 0;
aboutToAppear(): void {
this.loadMemos();
this.startAutoRefresh();
}
aboutToDisappear(): void {
this.stopAutoRefresh();
}
private loadMemos(): void {
this.memos = this.voiceService.getMemos();
}
private startAutoRefresh(): void {
this.timer = setInterval(() => {
this.loadMemos();
}, 60000); // 每分钟刷新一次
}
private stopAutoRefresh(): void {
if (this.timer) {
clearInterval(this.timer);
this.timer = 0;
}
}
build() {
Column() {
// 标题
Text('语音备忘录')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 20 });
// 录音控制按钮
Button(this.isRecording ? '停止录音' : '开始录音')
.type(ButtonType.Capsule)
.width('80%')
.height(50)
.backgroundColor(this.isRecording ? '#F44336' : '#4CAF50')
.fontColor('#FFFFFF')
.margin({ bottom: 20 })
.onClick(() => {
this.toggleRecording();
});
// 录音时长显示
if (this.isRecording) {
Text(`录音中... ${this.recordingDuration}秒`)
.fontSize(16)
.fontColor('#666666')
.margin({ bottom: 20 });
}
// 备忘录列表
if (this.memos.length > 0) {
List({ space: 10 }) {
ForEach(this.memos, (memo) => {
ListItem() {
Column() {
// 备忘录内容
Row() {
Image($r('app.media.ic_voice'))
.width(30)
.height(30)
.margin({ right: 10 });
Column() {
Text(memo.transcript.length > 30 ? memo.transcript.substring(0, 30) + '...' : memo.transcript)
.fontSize(16)
.margin({ bottom: 5 });
Row() {
Text(new Date(memo.timestamp).toLocaleString())
.fontSize(12)
.fontColor('#666666');
Text(`${memo.duration}秒`)
.fontSize(12)
.fontColor('#666666')
.margin({ left: 10 });
}
}
.layoutWeight(1);
// 播放按钮
Button() {
Image($r('app.media.ic_play'))
.width(20)
.height(20)
}
.type(ButtonType.Circle)
.width(40)
.height(40)
.backgroundColor('#2196F3')
.onClick(() => {
this.voiceService.playMemo(memo.id);
});
// 删除按钮
Button() {
Image($r('app.media.ic_delete'))
.width(20)
.height(20)
}
.type(ButtonType.Circle)
.width(40)
.height(40)
.backgroundColor('#F44336')
.margin({ left: 10 })
.onClick(() => {
this.deleteMemo(memo.id);
});
}
.width('100%')
.padding(10)
}
}
.borderRadius(10)
.backgroundColor('#FFFFFF')
.shadow({ radius: 5, color: '#E0E0E0', offsetX: 0, offsetY: 2 })
})
}
.width('100%')
.layoutWeight(1)
} else {
Column() {
Image($r('app.media.ic_empty'))
.width(100)
.height(100)
.margin({ bottom: 20 });
Text('暂无语音备忘录')
.fontSize(16)
.fontColor('#666666');
}
.width('100%')
.height('60%')
.justifyContent(FlexAlign.Center)
}
}
.width('100%')
.height('100%')
.padding(20);
}
private async toggleRecording(): Promise<void> {
if (this.isRecording) {
// 停止录音
const memo = await this.voiceService.stopRecording(this.currentRecordingUri!);
if (memo) {
prompt.showToast({ message: '备忘录已保存', duration: 2000 });
}
this.isRecording = false;
this.currentRecordingUri = null;
this.recordingDuration = 0;
this.loadMemos();
} else {
// 开始录音
const uri = await this.voiceService.startRecording();
if (uri) {
this.isRecording = true;
this.currentRecordingUri = uri;
this.startRecordingTimer();
}
}
}
private startRecordingTimer(): void {
this.recordingDuration = 0;
const timer = setInterval(() => {
this.recordingDuration++;
if (!this.isRecording) {
clearInterval(timer);
}
}, 1000);
}
private async deleteMemo(memoId: string): Promise<void> {
const success = await this.voiceService.deleteMemo(memoId);
if (success) {
prompt.showToast({ message: '备忘录已删除', duration: 2000 });
this.loadMemos();
} else {
prompt.showToast({ message: '删除失败', duration: 2000 });
}
}
}
3. 主界面实现
// src/main/ets/pages/MemoPage.ets
import { VoiceService } from '../service/VoiceService';
import { VoiceMemoList } from '../components/VoiceMemoList';
@Entry
@Component
struct MemoPage {
@State activeTab: number = 0;
private voiceService = VoiceService.getInstance();
build() {
Column() {
// 标题
Text('语音备忘录')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 20 });
// 标签页
Tabs({ barPosition: BarPosition.Start }) {
TabContent() {
// 备忘录列表标签页
VoiceMemoList()
}
.tabBar('我的备忘录');
TabContent() {
// 设置标签页
this.buildSettingsTab()
}
.tabBar('设置');
}
.barWidth('100%')
.barHeight(50)
.width('100%')
.height('80%')
}
.width('100%')
.height('100%')
.padding(20);
}
@Builder
private buildSettingsTab() {
Column() {
Text('存储设置')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 20 });
Row() {
Text('最大存储空间:')
.fontSize(16)
.margin({ right: 10 });
Text('100MB')
.fontSize(16)
.fontColor('#2196F3');
}
.margin({ bottom: 30 });
Text('设备同步')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 20 });
Row() {
Text('同步状态:')
.fontSize(16)
.margin({ right: 10 });
Text('已启用')
.fontSize(16)
.fontColor('#4CAF50');
}
.margin({ bottom: 30 });
Button('立即同步')
.type(ButtonType.Capsule)
.width('60%')
.height(40)
.backgroundColor('#2196F3')
.fontColor('#FFFFFF')
.onClick(() => {
this.voiceService.syncData();
prompt.showToast({ message: '同步已开始', duration: 2000 });
});
Button('清理存储空间')
.type(ButtonType.Capsule)
.width('60%')
.height(40)
.backgroundColor('#FF9800')
.fontColor('#FFFFFF')
.margin({ top: 30 })
.onClick(() => {
this.voiceService.cleanupStorage();
prompt.showToast({ message: '存储清理中...', duration: 2000 });
});
}
.width('100%')
.height('100%')
.padding(20);
}
}
四、与游戏同步技术的结合点
- 实时状态同步:借鉴游戏中玩家状态实时同步机制,优化备忘录的跨设备同步体验
- 冲突解决策略:采用类似游戏中的"时间戳优先"策略解决多设备数据冲突
- 资源优化管理:参考游戏中的资源加载策略,优化音频文件的存储和清理
- 设备角色分配:类似游戏中的主机/客户端模式,确定主录音设备和从属设备
- 批量数据传输:使用类似游戏中的网络优化技术,对语音数据进行批量压缩传输
五、关键特性实现
-
本地语音识别优化:
private async localSpeechRecognition(uri: string): Promise<string> { // 实际应用中应集成本地ASR引擎 // 这里简化为返回固定文本 return "这是语音备忘录的示例文本"; } -
CPU频率调节:
// 开始录音时优化CPU频率 await power.enablePowerMode(power.PowerMode.NORMAL, 'Voice recording in progress'); // 停止录音时恢复默认 await power.disablePowerMode(power.PowerMode.NORMAL); -
智能存储清理:
private async cleanupStorage(): Promise<void> { // 计算当前存储使用量 let totalSize = 0; const activeMemos = this.voiceMemos.filter(m => !m.isDeleted); for (const memo of activeMemos) { try { const stat = await fileIo.stat(memo.audioPath); totalSize += stat.size; } catch (e) { console.log(`Audio file not found: ${memo.audioPath}`); } } // 转换为MB const totalSizeMB = totalSize / (1024 * 1024); // 如果超过最大限制,清理旧文件 if (totalSizeMB > this.MAX_STORAGE_MB) { // 按时间排序,最早的在前 const sortedMemos = [...activeMemos].sort((a, b) => a.timestamp - b.timestamp); let freedSpace = 0; for (const memo of sortedMemos) { if (totalSizeMB - freedSpace <= this.MAX_STORAGE_MB * 0.9) { break; // 清理到90%容量停止 } try { const stat = await fileIo.stat(memo.audioPath); await fileIo.unlink(memo.audioPath); freedSpace += stat.size / (1024 * 1024); memo.isDeleted = true; } catch (e) { console.log(`Failed to delete audio file: ${memo.audioPath}`); } } } } -
分布式数据同步:
private handleRemoteDataChange(data: distributedData.ChangeData): void { data.insertEntries.forEach((entry: distributedData.Entry) => { if (entry.key === 'voice_memos') { const remoteMemos = entry.value.value as VoiceMemo[]; this.mergeVoiceMemos(remoteMemos); } }); }
六、性能优化策略
-
智能同步调度:
// 只有新数据时才触发同步 if (now - this.lastSyncTime > this.SYNC_INTERVAL && this.voiceMemos.some(m => !m.isSynced)) { this.syncData(); this.lastSyncTime = now; } -
本地缓存优先:
public getMemos(): VoiceMemo[] { // 先从内存缓存读取 return this.voiceMemos.filter(m => !m.isDeleted); } -
资源释放管理:
public async destroy(): Promise<void> { if (this.audioRecorder) { await this.audioRecorder.release(); this.audioRecorder = null; } if (this.kvStore) { this.kvStore.off('dataChange'); } await this.saveLocalData(); } -
批量数据处理:
// 同步时批量处理未同步的备忘录 const unsyncedMemos = this.voiceMemos.filter(m => !m.isSynced); if (unsyncedMemos.length > 0) { await this.kvStore.put('voice_memos', { value: unsyncedMemos }); }
七、项目扩展方向
- 语音指令识别:添加语音控制功能,如"删除最后一个备忘录"等
- 云端备份:支持将重要备忘录备份到云端
- 语音搜索:实现基于语音识别的备忘录搜索功能
- 多语言支持:增加多语言语音识别和转录
- 智能分类:基于内容自动分类备忘录(如工作、个人等)
八、总结
本文实现的离线语音备忘录应用具有以下特点:
- 采用本地语音识别引擎,保护用户隐私并实现离线使用
- 优化录音过程CPU频率调节,平衡性能和功耗
- 实现智能存储清理策略,自动管理设备存储空间
- 基于分布式数据同步技术,实现多设备间备忘录同步
- 提供直观的用户界面和流畅的操作体验
该应用展示了HarmonyOS在音频处理、资源管理和分布式能力方面的优势,为开发者提供了语音类应用开发的参考方案。通过借鉴游戏同步技术,实现了高效可靠的数据同步机制,确保了多设备间用户体验的一致性。
更多推荐



所有评论(0)