鸿蒙开发-参数管理

2 阅读4分钟

效果器参数怎么保存?HarmonyOS参数快照存储

如果你对吉他效果器感兴趣,可以去鸿蒙应用市场搜一下**「单块酷」**,下载下来体验体验。调好效果器参数后保存为快照,下次直接加载使用。体验完了再回来看这篇文章,你会更清楚参数快照功能是怎么实现的。


写在前面

大家好,我是一名写了十多年Web前端的老兵。从jQuery时代一路走到React/Vue,localStorage和状态管理是每天都在用的。去年开始转战鸿蒙生态,用ArkTS开发App,发现鸿蒙的preferences API非常适合存储配置类数据。

比如:

  • 配置存储:Web里用localStorage.setItem('config', JSON.stringify(data));鸿蒙里用preferences.put()flush()
  • 异步操作:Web里localStorage是同步的;鸿蒙里preferences全是异步的。
  • 数据持久化:Web里刷新页面就丢失内存数据;鸿蒙里flush后数据持久保存。

别担心,接下来这篇文章,我会用"单块酷"的参数快照功能,带你看看HarmonyOS怎么存储效果器参数配置。


这篇文章聊什么

单块酷的参数管理功能,核心要解决:

  1. 参数定义:每种效果器的参数类型和范围
  2. 快照保存:保存当前参数组合为快照
  3. 快照加载:加载历史快照恢复参数
  4. 快照管理:查看、删除、分享快照

第一步:设计参数数据结构

// Web前端同学看这里:参数管理的核心是"键值对"思想
// 每个效果器有多个参数,每个参数有名称、当前值、范围

// 效果器参数定义
interface PedalParam {
  name: string;
  label: string;
  min: number;
  max: number;
  step: number;
  default: number;
}

// 各效果器的参数定义
const PEDAL_PARAMS: Record<string, PedalParam[]> = {
  overdrive: [
    { name: 'gain', label: '增益', min: 0, max: 10, step: 1, default: 5 },
    { name: 'tone', label: '音色', min: 0, max: 10, step: 1, default: 5 },
    { name: 'level', label: '音量', min: 0, max: 10, step: 1, default: 5 },
  ],
  delay: [
    { name: 'time', label: '时间', min: 20, max: 1000, step: 10, default: 300 },
    { name: 'feedback', label: '反馈', min: 0, max: 100, step: 5, default: 30 },
    { name: 'mix', label: '混合', min: 0, max: 100, step: 5, default: 50 },
  ],
  reverb: [
    { name: 'decay', label: '衰减', min: 0, max: 100, step: 5, default: 50 },
    { name: 'mix', label: '混合', min: 0, max: 100, step: 5, default: 30 },
    { name: 'predelay', label: '预延迟', min: 0, max: 100, step: 5, default: 10 },
  ],
  chorus: [
    { name: 'rate', label: '速率', min: 0, max: 10, step: 1, default: 5 },
    { name: 'depth', label: '深度', min: 0, max: 10, step: 1, default: 5 },
    { name: 'mix', label: '混合', min: 0, max: 100, step: 5, default: 50 },
  ],
};

// 参数快照
interface ParamSnapshot {
  id: string;
  name: string;
  description: string;
  chainId: string;
  params: Record<string, Record<string, number>>; // { pedalId: { paramName: value } }
  createdAt: string;
}

第二步:实现参数快照存储

// Web前端同学看这里:Web里我们用localStorage存储JSON
// 鸿蒙里用preferences,记得调用flush()持久化

import { preferences } from '@kit.ArkData';

let prefInstance: preferences.Preferences | null = null;

async function getPreferences(context: Context): Promise<preferences.Preferences> {
  if (!prefInstance) {
    prefInstance = await preferences.getPreferences(context, 'dankuaiku_data');
  }
  return prefInstance;
}

async function setItem(context: Context, key: string, value: unknown): Promise<boolean> {
  try {
    const pref = await getPreferences(context);
    await pref.put(key, JSON.stringify(value));
    await pref.flush();
    return true;
  } catch (err) {
    console.error('存储失败:', err);
    return false;
  }
}

async function getItem<T>(context: Context, key: string, defaultValue: T): Promise<T> {
  try {
    const pref = await getPreferences(context);
    const value = await pref.get(key, '');
    if (typeof value === 'string' && value.length > 0) {
      return JSON.parse(value) as T;
    }
    return defaultValue;
  } catch (err) {
    return defaultValue;
  }
}

// 保存参数快照
async function saveSnapshot(context: Context, snapshot: ParamSnapshot): Promise<boolean> {
  const snapshots = await getItem<ParamSnapshot[]>(context, 'snapshots', []);

  const newSnapshot: ParamSnapshot = {
    ...snapshot,
    id: `snapshot_${Date.now()}`,
    createdAt: new Date().toISOString().slice(0, 10)
  };

  snapshots.push(newSnapshot);
  return await setItem(context, 'snapshots', snapshots);
}

// 获取所有快照
async function getSnapshots(context: Context): Promise<ParamSnapshot[]> {
  return await getItem<ParamSnapshot[]>(context, 'snapshots', []);
}

// 删除快照
async function deleteSnapshot(context: Context, id: string): Promise<boolean> {
  const snapshots = await getItem<ParamSnapshot[]>(context, 'snapshots', []);
  const filtered = snapshots.filter(s => s.id !== id);
  return await setItem(context, 'snapshots', filtered);
}

// 加载快照到效果链
async function loadSnapshot(context: Context, snapshotId: string): Promise<Record<string, Record<string, number>> | null> {
  const snapshots = await getItem<ParamSnapshot[]>(context, 'snapshots', []);
  const snapshot = snapshots.find(s => s.id === snapshotId);
  return snapshot?.params || null;
}

第三步:实现参数调节页面

// Web前端同学看这里:React里我们用state管理参数值
// 鸿蒙里用@State装饰器,Slider组件调节参数值

@Entry
@Component
struct ParamEditorPage {
  @State currentParams: Record<string, Record<string, number>> = {}
  @State selectedPedal: string = ''
  @State snapshotName: string = ''

  private chain: PedalChain | null = null

  async aboutToAppear() {
    // 加载效果链和当前参数
    if (this.chain && this.chain.pedals.length > 0) {
      this.selectedPedal = this.chain.pedals[0].id;
      this.initParams();
    }
  }

  private initParams() {
    if (!this.chain) return;

    this.chain.pedals.forEach(pedal => {
      const paramDefs = PEDAL_PARAMS[pedal.typeId] || [];
      this.currentParams[pedal.id] = {};
      paramDefs.forEach(param => {
        this.currentParams[pedal.id][param.name] = pedal.parameters[param.name] ?? param.default;
      });
    });
  }

  private updateParam(pedalId: string, paramName: string, value: number) {
    if (!this.currentParams[pedalId]) {
      this.currentParams[pedalId] = {};
    }
    this.currentParams[pedalId][paramName] = value;
  }

  async saveCurrentSnapshot() {
    if (!this.chain) return;

    const snapshot: ParamSnapshot = {
      id: '',
      name: this.snapshotName || `快照 ${Date.now()}`,
      description: '',
      chainId: this.chain.id,
      params: this.currentParams,
      createdAt: ''
    };

    const success = await saveSnapshot(getContext(this) as Context, snapshot);
    if (success) {
      this.snapshotName = '';
    }
  }

  build() {
    Column() {
      Text('参数调节')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .margin({ bottom: 16 })

      // 效果器选择
      Scroll() {
        Row() {
          ForEach(this.chain?.pedals || [], (pedal: PedalInstance) => {
            const typeInfo = PEDAL_TYPES.find(t => t.id === pedal.typeId);
            Button(typeInfo?.name || '')
              .fontSize(12)
              .height(36)
              .backgroundColor(this.selectedPedal === pedal.id ? typeInfo?.color : '#f3f4f6')
              .fontColor(this.selectedPedal === pedal.id ? '#ffffff' : '#374151')
              .borderRadius(8)
              .margin({ right: 8 })
              .onClick(() => { this.selectedPedal = pedal.id })
          })
        }
      }
      .scrollable(ScrollDirection.Horizontal)
      .width('100%')
      .margin({ bottom: 16 })

      // 参数滑块
      if (this.selectedPedal) {
        const pedalType = this.chain?.pedals.find(p => p.id === this.selectedPedal)?.typeId || '';
        const paramDefs = PEDAL_PARAMS[pedalType] || [];

        ForEach(paramDefs, (param: PedalParam) => {
          Row() {
            Text(param.label)
              .fontSize(14)
              .width(60)

            Slider({
              value: this.currentParams[this.selectedPedal]?.[param.name] ?? param.default,
              min: param.min,
              max: param.max,
              step: param.step
            })
              .layoutWeight(1)
              .onChange((value: number) => {
                this.updateParam(this.selectedPedal, param.name, value);
              })

            Text(`${this.currentParams[this.selectedPedal]?.[param.name] ?? param.default}`)
              .fontSize(14)
              .width(40)
              .textAlign(TextAlign.End)
          }
          .width('100%')
          .margin({ bottom: 12 })
        })
      }

      // 保存快照
      TextInput({ placeholder: '快照名称', text: this.snapshotName })
        .width('100%')
        .height(44)
        .margin({ top: 16 })
        .onChange((value: string) => { this.snapshotName = value })

      Button('保存快照')
        .width('100%')
        .height(48)
        .backgroundColor('#1f2937')
        .borderRadius(12)
        .margin({ top: 8 })
        .onClick(() => this.saveCurrentSnapshot())
    }
    .padding(16)
  }
}

第四步:常见问题

4.1 参数范围验证

问题:用户输入超出范围的参数值。

解决:Slider组件本身有min/max限制,TextInput需要额外验证。

4.2 快照数量管理

问题:快照太多难以查找。

解决:支持按效果链分组查看,或提供搜索功能。


总结

这篇文章围绕"单块酷"的参数快照功能,讲解了:

参数管理

  • 参数定义和范围
  • 参数调节UI(Slider)
  • 参数值的实时更新

快照存储

  • preferences存储参数组合
  • 快照的保存和加载
  • 快照列表管理

如果你对"单块酷"感兴趣,欢迎去鸿蒙应用市场搜索下载体验。