鸿蒙5蓝牙MIDI音乐合成器开发指南 原创

进修的泡芙
发布于 2025-6-20 14:25
浏览
0收藏

鸿蒙5蓝牙MIDI音乐合成器开发指南

一、项目概述

本文基于HarmonyOS 5的蓝牙MIDI能力和分布式技术,开发一款多设备协同的音乐合成器,借鉴《鸿蒙跨端U同步》中游戏多设备同步的技术原理,实现电子乐器间的实时编曲与演奏同步。该系统支持通过蓝牙MIDI连接多个电子乐器设备,实现音符数据的实时传输、多轨道合成和分布式同步播放。

二、系统架构

±--------------------+ ±--------------------+ ±--------------------+
主控合成器 <-----> 分布式数据总线 <-----> 从属乐器设备
(Master Synth) (Distributed Bus) (Slave Device)
±---------±---------+ ±---------±---------+ ±---------±---------+

±---------v----------+ ±---------v----------+ ±---------v----------+
MIDI音序引擎 演奏同步服务 MIDI输入处理
(Sequencer Engine) (Sync Service) (MIDI Processor)

±--------------------+ ±--------------------+ ±--------------------+

三、核心代码实现
MIDI数据模型

// src/main/ets/model/MidiDataModel.ts
export class MidiMessage {
type: MidiMessageType; // 消息类型
channel: number; // MIDI通道(0-15)
note?: number; // 音符编号(0-127)
velocity?: number; // 力度(0-127)
control?: number; // 控制器编号
value?: number; // 控制器值
timestamp: number; // 时间戳(毫秒)
deviceId: string; // 来源设备ID

constructor(type: MidiMessageType, channel: number) {
this.type = type;
this.channel = channel;
this.timestamp = Date.now();
this.deviceId = ‘’;
static createNoteOn(channel: number, note: number, velocity: number): MidiMessage {

const msg = new MidiMessage(MidiMessageType.NOTE_ON, channel);
msg.note = note;
msg.velocity = velocity;
return msg;

static createNoteOff(channel: number, note: number): MidiMessage {

const msg = new MidiMessage(MidiMessageType.NOTE_OFF, channel);
msg.note = note;
return msg;

static createControlChange(channel: number, control: number, value: number): MidiMessage {

const msg = new MidiMessage(MidiMessageType.CONTROL_CHANGE, channel);
msg.control = control;
msg.value = value;
return msg;

toJson(): string {

return JSON.stringify({
  type: this.type,
  channel: this.channel,
  note: this.note,
  velocity: this.velocity,
  control: this.control,
  value: this.value,
  timestamp: this.timestamp,
  deviceId: this.deviceId
});

static fromJson(jsonStr: string): MidiMessage {

const json = JSON.parse(jsonStr);
let msg: MidiMessage;

switch (json.type) {
  case MidiMessageType.NOTE_ON:
    msg = MidiMessage.createNoteOn(json.channel, json.note, json.velocity);
    break;
  case MidiMessageType.NOTE_OFF:
    msg = MidiMessage.createNoteOff(json.channel, json.note);
    break;
  case MidiMessageType.CONTROL_CHANGE:
    msg = MidiMessage.createControlChange(json.channel, json.control, json.value);
    break;
  default:
    msg = new MidiMessage(json.type, json.channel);

msg.timestamp = json.timestamp;

msg.deviceId = json.deviceId;
return msg;

}

export enum MidiMessageType {
NOTE_ON = 0x90,
NOTE_OFF = 0x80,
CONTROL_CHANGE = 0xB0,
PROGRAM_CHANGE = 0xC0,
PITCH_BEND = 0xE0
export class MidiTrack {

id: string;
name: string;
channel: number;
deviceId: string;
messages: MidiMessage[];
instrument: MidiInstrument;
volume: number;
pan: number;
muted: boolean;

constructor(name: string, channel: number) {
this.id = this.generateId();
this.name = name;
this.channel = channel;
this.deviceId = ‘’;
this.messages = [];
this.instrument = MidiInstrument.ACOUSTIC_GRAND_PIANO;
this.volume = 100;
this.pan = 64;
this.muted = false;
private generateId(): string {

return 'track_' + Math.random().toString(36).substring(2, 9);

addMessage(message: MidiMessage): void {

this.messages.push(message);

clear(): void {

this.messages = [];

toJson(): string {

return JSON.stringify({
  id: this.id,
  name: this.name,
  channel: this.channel,
  deviceId: this.deviceId,
  messages: this.messages.map(msg => msg.toJson()),
  instrument: this.instrument,
  volume: this.volume,
  pan: this.pan,
  muted: this.muted
});

static fromJson(jsonStr: string): MidiTrack {

const json = JSON.parse(jsonStr);
const track = new MidiTrack(json.name, json.channel);
track.id = json.id;
track.deviceId = json.deviceId;
track.messages = json.messages.map((msg: string) => MidiMessage.fromJson(msg));
track.instrument = json.instrument;
track.volume = json.volume;
track.pan = json.pan;
track.muted = json.muted;
return track;

}

export enum MidiInstrument {
ACOUSTIC_GRAND_PIANO = 0,
BRIGHT_ACOUSTIC_PIANO = 1,
ELECTRIC_GRAND_PIANO = 2,
// … 其他GM音色
GUITAR = 24,
BASS = 32,
VIOLIN = 40,
TRUMPET = 56,
SAXOPHONE = 64,
FLUTE = 73,
SYNTH_LEAD = 80,
SYNTH_PAD = 88

分布式同步服务

// src/main/ets/service/DistributedSyncService.ts
import { distributedData } from ‘@ohos.data.distributedData’;
import { BusinessError } from ‘@ohos.base’;
import { MidiMessage, MidiTrack } from ‘…/model/MidiDataModel’;
import { deviceManager } from ‘@ohos.distributedDeviceManager’;

export class DistributedSyncService {
private static instance: DistributedSyncService;
private kvManager: distributedData.KVManager;
private kvStore: distributedData.KVStore;
private readonly STORE_ID = ‘midi_sync_store’;
private readonly MESSAGE_KEY_PREFIX = ‘midi_msg_’;
private readonly TRACK_KEY_PREFIX = ‘midi_track_’;
private messageSubscribers: ((data: MidiMessage) => void)[] = [];
private trackSubscribers: ((data: MidiTrack) => void)[] = [];

private constructor() {
this.initDistributedData();
public static getInstance(): DistributedSyncService {

if (!DistributedSyncService.instance) {
  DistributedSyncService.instance = new DistributedSyncService();

return DistributedSyncService.instance;

private initDistributedData(): void {

const config: distributedData.KVManagerConfig = {
  bundleName: 'com.example.midisynth',
  userInfo: {
    userId: '0',
    userType: distributedData.UserType.SAME_USER_ID

};

try {
  distributedData.createKVManager(config, (err: BusinessError, manager: distributedData.KVManager) => {
    if (err) {
      console.error(Failed to create KVManager. Code: {err.code}, message: {err.message});
      return;

this.kvManager = manager;

    const options: distributedData.Options = {
      createIfMissing: true,
      encrypt: true,
      backup: false,
      autoSync: true,
      kvStoreType: distributedData.KVStoreType.SINGLE_VERSION,
      schema: '',
      securityLevel: distributedData.SecurityLevel.S2
    };

    this.kvManager.getKVStore(this.STORE_ID, options, (err: BusinessError, store: distributedData.KVStore) => {
      if (err) {
        console.error(Failed to get KVStore. Code: {err.code}, message: {err.message});
        return;

this.kvStore = store;

      this.registerDataListeners();
    });
  });

catch (e) {

  console.error(An unexpected error occurred. Code: {e.code}, message: {e.message});

}

private registerDataListeners(): void {
try {
// MIDI消息监听
this.kvStore.on(‘dataChange’, distributedData.SubscribeType.SUBSCRIBE_TYPE_ALL, (data: distributedData.ChangeData) => {
if (data.key.startsWith(this.MESSAGE_KEY_PREFIX)) {
const message = MidiMessage.fromJson(data.value.value as string);
this.notifyMessageSubscribers(message);
else if (data.key.startsWith(this.TRACK_KEY_PREFIX)) {

      const track = MidiTrack.fromJson(data.value.value as string);
      this.notifyTrackSubscribers(track);

});

catch (e) {

  console.error(Failed to register data listeners. Code: {e.code}, message: {e.message});

}

public subscribeMessage(callback: (data: MidiMessage) => void): void {
this.messageSubscribers.push(callback);
public unsubscribeMessage(callback: (data: MidiMessage) => void): void {

this.messageSubscribers = this.messageSubscribers.filter(sub => sub !== callback);

public subscribeTrack(callback: (data: MidiTrack) => void): void {

this.trackSubscribers.push(callback);

public unsubscribeTrack(callback: (data: MidiTrack) => void): void {

this.trackSubscribers = this.trackSubscribers.filter(sub => sub !== callback);

private notifyMessageSubscribers(data: MidiMessage): void {

this.messageSubscribers.forEach(callback => callback(data));

private notifyTrackSubscribers(data: MidiTrack): void {

this.trackSubscribers.forEach(callback => callback(data));

public syncMidiMessage(message: MidiMessage): void {

if (!this.kvStore) {
  console.error('KVStore is not initialized');
  return;

deviceManager.getLocalDeviceInfo((err: BusinessError, info) => {

  if (err) {
    console.error(Failed to get device info. Code: {err.code}, message: {err.message});
    return;

message.deviceId = info.deviceId;

  const key = this.MESSAGE_KEY_PREFIX + message.timestamp + '_' + Math.random().toString(36).substring(2, 6);

  try {
    this.kvStore.put(key, message.toJson(), (err: BusinessError) => {
      if (err) {
        console.error(Failed to put message. Code: {err.code}, message: {err.message});

});

catch (e) {

    console.error(An unexpected error occurred. Code: {e.code}, message: {e.message});

});

public syncMidiTrack(track: MidiTrack): void {

if (!this.kvStore) {
  console.error('KVStore is not initialized');
  return;

deviceManager.getLocalDeviceInfo((err: BusinessError, info) => {

  if (err) {
    console.error(Failed to get device info. Code: {err.code}, message: {err.message});
    return;

track.deviceId = info.deviceId;

  const key = this.TRACK_KEY_PREFIX + track.id;

  try {
    this.kvStore.put(key, track.toJson(), (err: BusinessError) => {
      if (err) {
        console.error(Failed to put track. Code: {err.code}, message: {err.message});

});

catch (e) {

    console.error(An unexpected error occurred. Code: {e.code}, message: {e.message});

});

public async getAllTracks(): Promise<MidiTrack[]> {

return new Promise((resolve) => {
  if (!this.kvStore) {
    resolve([]);
    return;

try {

    const query: distributedData.Query = {
      prefixKey: this.TRACK_KEY_PREFIX
    };

    this.kvStore.getEntries(query, (err: BusinessError, entries: distributedData.Entry[]) => {
      if (err) {
        console.error(Failed to get entries. Code: {err.code}, message: {err.message});
        resolve([]);
        return;

const tracks = entries.map(entry =>

        MidiTrack.fromJson(entry.value.value as string)
      );
      resolve(tracks);
    });

catch (e) {

    console.error(An unexpected error occurred. Code: {e.code}, message: {e.message});
    resolve([]);

});

}

MIDI合成引擎

// src/main/ets/engine/MidiSynthEngine.ts
import { MidiMessage, MidiTrack, MidiInstrument } from ‘…/model/MidiDataModel’;
import { audio } from ‘@ohos.multimedia.audio’;
import { BusinessError } from ‘@ohos.base’;

export class MidiSynthEngine {
private audioRenderer: audio.AudioRenderer | null = null;
private audioStreamInfo: audio.AudioStreamInfo;
private audioRendererInfo: audio.AudioRendererInfo;
private sampleRate = 44100;
private voices: Map<number, Voice> = new Map();
private tracks: Map<string, MidiTrack> = new Map();
private globalBpm = 120;
private isPlaying = false;
private playStartTime = 0;
private currentPlayPosition = 0;

constructor() {
this.audioStreamInfo = {
samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_44100,
channels: audio.AudioChannel.CHANNEL_2,
sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_F32LE,
encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW
};

this.audioRendererInfo = {
  content: audio.ContentType.CONTENT_TYPE_MUSIC,
  usage: audio.StreamUsage.STREAM_USAGE_MEDIA,
  rendererFlags: 0
};

this.initAudioRenderer();

private async initAudioRenderer(): Promise<void> {

try {
  this.audioRenderer = await audio.createAudioRenderer(this.audioRendererInfo, this.audioStreamInfo);
  await this.audioRenderer.start();
  
  // 启动音频渲染线程
  this.renderAudio();

catch (e) {

  console.error(Failed to init audio renderer. Code: {e.code}, message: {e.message});

}

private renderAudio(): void {
if (!this.audioRenderer) return;

const bufferSize = this.audioRenderer.getBufferSize();
const buffer = new ArrayBuffer(bufferSize);
const floatView = new Float32Array(buffer);

const render = () => {
  if (!this.isPlaying) {
    for (let i = 0; i < floatView.length; i++) {
      floatView[i] = 0; // 静音

} else {

    const now = Date.now();
    const deltaTime = now - this.playStartTime;
    this.currentPlayPosition = deltaTime * (this.globalBpm / 60000);
    
    // 合成所有活跃的声音
    for (let i = 0; i < floatView.length; i += 2) {
      let left = 0;
      let right = 0;
      
      this.voices.forEach(voice => {
        const sample = voice.nextSample();
        left += sample * (1 - voice.pan);
        right += sample * voice.pan;
      });
      
      floatView[i] = left * 0.2; // 降低音量避免削波
      floatView[i + 1] = right * 0.2;

}

  this.audioRenderer?.write(buffer).then(() => {
    requestAnimationFrame(render);
  }).catch((e: BusinessError) => {
    console.error(Audio render error. Code: {e.code}, message: {e.message});
  });
};

requestAnimationFrame(render);

public addTrack(track: MidiTrack): void {

this.tracks.set(track.id, track);

public removeTrack(trackId: string): void {

this.tracks.delete(trackId);

public play(): void {

this.isPlaying = true;
this.playStartTime = Date.now() - this.currentPlayPosition * (60000 / this.globalBpm);

public pause(): void {

this.isPlaying = false;

public stop(): void {

this.isPlaying = false;
this.currentPlayPosition = 0;
this.voices.clear();

public setBpm(bpm: number): void {

this.globalBpm = bpm;

public handleMidiMessage(message: MidiMessage): void {

switch (message.type) {
  case MidiMessageType.NOTE_ON:
    this.noteOn(message.channel, message.note!, message.velocity!);
    break;
  case MidiMessageType.NOTE_OFF:
    this.noteOff(message.channel, message.note!);
    break;
  // 其他MIDI消息处理...

}

private noteOn(channel: number, note: number, velocity: number): void {
const voiceId = channel * 128 + note;
if (this.voices.has(voiceId)) {
this.voices.get(voiceId)?.stop();
const track = Array.from(this.tracks.values()).find(t => t.channel === channel);

const instrument = track?.instrument || MidiInstrument.ACOUSTIC_GRAND_PIANO;
const volume = (track?.volume || 100) / 100;
const pan = (track?.pan || 64) / 127;

const voice = new Voice(note, velocity * volume, pan, instrument);
this.voices.set(voiceId, voice);

private noteOff(channel: number, note: number): void {

const voiceId = channel * 128 + note;
this.voices.get(voiceId)?.release();
setTimeout(() => {
  this.voices.delete(voiceId);
}, 1000); // 留出释放时间

}

class Voice {
private note: number;
private velocity: number;
private pan: number;
private instrument: MidiInstrument;
private phase = 0;
private envStage: ‘attack’ ‘decay’ ‘sustain’
‘release’ = ‘attack’;
private envLevel = 0;
private released = false;

constructor(note: number, velocity: number, pan: number, instrument: MidiInstrument) {
this.note = note;
this.velocity = velocity;
this.pan = pan;
this.instrument = instrument;
nextSample(): number {

const freq = 440 * Math.pow(2, (this.note - 69) / 12);
const increment = (2  Math.PI  freq) / 44100;

// 简单合成器 - 正弦波
let sample = Math.sin(this.phase)  this.velocity  this.getEnvelope();
this.phase += increment;

// 简单ADSR包络
this.updateEnvelope();

return sample;

private getEnvelope(): number {

return this.envLevel;

private updateEnvelope(): void {

switch (this.envStage) {
  case 'attack':
    this.envLevel += 0.01;
    if (this.envLevel >= 1) {
      this.envLevel = 1;
      this.envStage = 'decay';

break;

  case 'decay':
    this.envLevel -= 0.001;
    if (this.envLevel <= 0.7) {
      this.envLevel = 0.7;
      this.envStage = 'sustain';

break;

  case 'sustain':
    if (this.released) {
      this.envStage = 'release';

break;

  case 'release':
    this.envLevel -= 0.002;
    if (this.envLevel <= 0) {
      this.envLevel = 0;

break;

}

release(): void {
this.released = true;
stop(): void {

this.envLevel = 0;

}

蓝牙MIDI服务

// src/main/ets/service/BluetoothMidiService.ts
import { bluetooth } from ‘@ohos.bluetooth’;
import { BusinessError } from ‘@ohos.base’;
import { MidiMessage, MidiMessageType } from ‘…/model/MidiDataModel’;
import { DistributedSyncService } from ‘./DistributedSyncService’;

export class BluetoothMidiService {
private static instance: BluetoothMidiService;
private midiDevices: Map<string, bluetooth.BluetoothDevice> = new Map();
private syncService = DistributedSyncService.getInstance();

private constructor() {
this.initBluetooth();
public static getInstance(): BluetoothMidiService {

if (!BluetoothMidiService.instance) {
  BluetoothMidiService.instance = new BluetoothMidiService();

return BluetoothMidiService.instance;

private initBluetooth(): void {

try {
  // 启用蓝牙
  bluetooth.enableBluetooth();

  // 注册蓝牙状态监听
  bluetooth.on('stateChange', (state: number) => {
    console.log(Bluetooth state changed: ${state});
  });

  // 注册设备发现监听
  bluetooth.on('deviceDiscover', (device: bluetooth.BluetoothDevice) => {
    if (device.name?.includes('MIDI') || device.deviceType === bluetooth.DeviceType.DEVICE_TYPE_LE_AUDIO) {
      this.midiDevices.set(device.deviceId, device);
      console.log(Found MIDI device: ${device.name});

});

catch (e) {

  console.error(Failed to init Bluetooth. Code: {e.code}, message: {e.message});

}

public async connectToDevice(deviceId: string): Promise<boolean> {
try {
const device = this.midiDevices.get(deviceId);
if (!device) {
console.error(‘Device not found’);
return false;
// 创建GATT连接

  await bluetooth.createGattConnection(deviceId);

  // 发现服务(MIDI服务UUID: 03B80E5A-EDE8-4B33-A751-6CE34EC4C700)
  const services = await bluetooth.getServices(deviceId);
  const midiService = services.find(s => s.serviceUuid === '03B80E5A-EDE8-4B33-A751-6CE34EC4C700');
  
  if (!midiService) {
    console.error('MIDI service not found');
    return false;

// 订阅MIDI特征值通知(MIDI数据特征UUID: 7772E5DB-3868-4112-A1A9-F2669D106BF3)

  const characteristics = await bluetooth.getCharacteristics(deviceId, midiService.serviceUuid);
  const midiCharacteristic = characteristics.find(c => c.characteristicUuid === '7772E5DB-3868-4112-A1A9-F2669D106BF3');
  
  if (!midiCharacteristic) {
    console.error('MIDI characteristic not found');
    return false;

await bluetooth.setCharacteristicNotification(deviceId, midiService.serviceUuid, midiCharacteristic.characteristicUuid, true);

  // 注册数据接收回调
  bluetooth.on('characteristicValueChange', (value: ArrayBuffer) => {
    this.handleMidiData(value, deviceId);
  });

  return true;

catch (e) {

  console.error(Failed to connect to device. Code: {e.code}, message: {e.message});
  return false;

}

private handleMidiData(data: ArrayBuffer, deviceId: string): void {
const view = new DataView(data);
const statusByte = view.getUint8(0);
const messageType = statusByte & 0xF0;
const channel = statusByte & 0x0F;

let message: MidiMessage;

switch (messageType) {
  case MidiMessageType.NOTE_ON:
    const note = view.getUint8(1);
    const velocity = view.getUint8(2);
    message = MidiMessage.createNoteOn(channel, note, velocity);
    break;
  case MidiMessageType.NOTE_OFF:
    const noteOff = view.getUint8(1);
    message = MidiMessage.createNoteOff(channel, noteOff);
    break;
  case MidiMessageType.CONTROL_CHANGE:
    const control = view.getUint8(1);
    const value = view.getUint8(2);
    message = MidiMessage.createControlChange(channel, control, value);
    break;
  default:
    return; // 忽略其他消息

message.deviceId = deviceId;

this.syncService.syncMidiMessage(message);

public async scanForDevices(duration: number = 10000): Promise<bluetooth.BluetoothDevice[]> {

try {
  this.midiDevices.clear();
  bluetooth.startBluetoothDiscovery();
  
  return new Promise((resolve) => {
    setTimeout(() => {
      bluetooth.stopBluetoothDiscovery();
      resolve(Array.from(this.midiDevices.values()));
    }, duration);
  });

catch (e) {

  console.error(Failed to scan for devices. Code: {e.code}, message: {e.message});
  return [];

}

public getConnectedDevices(): bluetooth.BluetoothDevice[] {
return Array.from(this.midiDevices.values()).filter(device => device.isConnected);
}

主控合成器界面

// src/main/ets/pages/MasterSynthView.ets
import { MidiMessage, MidiTrack, MidiInstrument } from ‘…/model/MidiDataModel’;
import { BluetoothMidiService } from ‘…/service/BluetoothMidiService’;
import { DistributedSyncService } from ‘…/service/DistributedSyncService’;
import { MidiSynthEngine } from ‘…/engine/MidiSynthEngine’;
import { bluetooth } from ‘@ohos.bluetooth’;

@Entry
@Component
struct MasterSynthView {
@State devices: bluetooth.BluetoothDevice[] = [];
@State tracks: MidiTrack[] = [];
@State selectedTrack: MidiTrack | null = null;
@State isPlaying: boolean = false;
@State bpm = 120;
private midiService = BluetoothMidiService.getInstance();
private syncService = DistributedSyncService.getInstance();
private synthEngine = new MidiSynthEngine();

aboutToAppear(): void {
this.syncService.subscribeTrack(this.handleTrackUpdate.bind(this));
this.syncService.subscribeMessage(this.handleMidiMessage.bind(this));
this.loadTracks();
aboutToDisappear(): void {

this.syncService.unsubscribeTrack(this.handleTrackUpdate.bind(this));
this.syncService.unsubscribeMessage(this.handleMidiMessage.bind(this));

private handleTrackUpdate(track: MidiTrack): void {

const index = this.tracks.findIndex(t => t.id === track.id);
if (index >= 0) {
  this.tracks[index] = track;

else {

  this.tracks.push(track);

this.tracks = […this.tracks];

this.synthEngine.addTrack(track);

private handleMidiMessage(message: MidiMessage): void {

this.synthEngine.handleMidiMessage(message);

private async loadTracks(): Promise<void> {

this.tracks = await this.syncService.getAllTracks();
this.tracks.forEach(track => this.synthEngine.addTrack(track));

private async scanDevices(): Promise<void> {

this.devices = await this.midiService.scanForDevices();

private async connectDevice(deviceId: string): Promise<void> {

const success = await this.midiService.connectToDevice(deviceId);
if (success) {
  // 为新设备创建轨道
  const device = this.devices.find(d => d.deviceId === deviceId);
  const track = new MidiTrack(device?.name || 设备 ${deviceId.substring(0, 4)}, this.tracks.length);
  this.syncService.syncMidiTrack(track);

}

private togglePlay(): void {
if (this.isPlaying) {
this.synthEngine.pause();
else {

  this.synthEngine.play();

this.isPlaying = !this.isPlaying;

private stopPlayback(): void {

this.synthEngine.stop();
this.isPlaying = false;

private updateBpm(value: number): void {

this.bpm = value;
this.synthEngine.setBpm(value);

private updateTrackInstrument(track: MidiTrack, instrument: MidiInstrument): void {

track.instrument = instrument;
this.syncService.syncMidiTrack(track);

build() {

Column() {
  // 设备列表
  Row() {
    Text('MIDI设备')
      .fontSize(18)
      .fontWeight(FontWeight.Bold)
    
    Button('扫描')
      .margin({left: 10})
      .onClick(() => this.scanDevices())

.width(‘100%’)

  .margin({bottom: 10})

  List({ space: 5 }) {
    ForEach(this.devices, (device) => {
      ListItem() {
        Row() {
          Text(device.name || '未知设备')
            .fontSize(16)
          
          Text(device.isConnected ? '已连接' : '未连接')
            .fontSize(14)
            .fontColor(device.isConnected ? '#4CAF50' : '#9E9E9E')
            .margin({left: 10})

.width(‘100%’)

        .padding(10)

.onClick(() => this.connectDevice(device.deviceId))

    })

.height(150)

  .width('100%')
  .margin({bottom: 20})

  // 控制面板
  Row() {
    Button(this.isPlaying ? '暂停' : '播放')
      .width(100)
      .onClick(() => this.togglePlay())
    
    Button('停止')
      .width(100)
      .margin({left: 10})
      .onClick(() => this.stopPlayback())
    
    Text(BPM: ${this.bpm})
      .margin({left: 20})
    
    Slider({
      value: this.bpm,
      min: 40,
      max: 240,
      step: 1,
      style: SliderStyle.OutSet
    })
    .width(150)
    .margin({left: 10})
    .onChange(v => this.updateBpm(v))

.width(‘100%’)

  .margin({bottom: 20})

  // 轨道列表
  Text('轨道')
    .fontSize(18)
    .fontWeight(FontWeight.Bold)
    .margin({bottom: 10})
  
  List({ space: 10 }) {
    ForEach(this.tracks, (track) => {
      ListItem() {
        Column() {
          Row() {
            Text(track.name)
              .fontSize(16)
              .fontWeight(FontWeight.Bold)
            
            Text(通道 ${track.channel + 1})
              .fontSize(14)
              .margin({left: 10})

Row() {

            Text(MidiInstrument[track.instrument])
              .fontSize(14)
            
            Text(设备: ${track.deviceId.substring(0, 6)})
              .fontSize(12)
              .opacity(0.7)
              .margin({left: 10})

.margin({top: 5})

.width(‘100%’)

        .padding(10)
        .borderRadius(10)
        .backgroundColor(this.selectedTrack?.id === track.id ? '#E3F2FD' : '#FAFAFA')

.onClick(() => {

        this.selectedTrack = track;
      })
    })

.width(‘100%’)

  .layoutWeight(1)

  // 轨道控制
  if (this.selectedTrack) {
    Column() {
      Text(this.selectedTrack.name)
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
      
      Row() {
        Text('乐器:')
          .fontSize(16)
        
        Select([

value: MidiInstrument.ACOUSTIC_GRAND_PIANO, name: ‘钢琴’ },

value: MidiInstrument.GUITAR, name: ‘吉他’ },

value: MidiInstrument.BASS, name: ‘贝斯’ },

value: MidiInstrument.SYNTH_LEAD, name: ‘合成器’ },

value: MidiInstrument.FLUTE, name: ‘长笛’ }

        ], this.selectedTrack.instrument)
        .onSelect((index: number) => {
          const instruments = [
            MidiInstrument.ACOUSTIC_GRAND_PIANO,
            MidiInstrument.GUITAR,
            MidiInstrument.BASS,
            MidiInstrument.SYNTH_LEAD,
            MidiInstrument.FLUTE
          ];
          this.updateTrackInstrument(this.selectedTrack!, instruments[index]);
        })

.margin({top: 10})

.width(‘100%’)

    .padding(15)
    .margin({top: 10})
    .borderRadius(10)
    .backgroundColor('#FFFFFF')
    .shadow({ radius: 5, color: '#E0E0E0', offsetX: 0, offsetY: 2 })

}

.width('100%')
.height('100%')
.padding(15)
.backgroundColor('#F5F5F5')

}

从属乐器界面

// src/main/ets/pages/SlaveDeviceView.ets
import { BluetoothMidiService } from ‘…/service/BluetoothMidiService’;
import { bluetooth } from ‘@ohos.bluetooth’;

@Entry
@Component
struct SlaveDeviceView {
@State deviceName: string = ‘MIDI设备’;
@State isConnected: boolean = false;
private midiService = BluetoothMidiService.getInstance();

aboutToAppear(): void {
deviceManager.getLocalDeviceInfo((err, info) => {
if (!err) {
this.deviceName = info.deviceName;
});

private async connectToMaster(): Promise<void> {

const devices = await this.midiService.scanForDevices();
const master = devices.find(d => d.name?.includes('Master'));

if (master) {
  this.isConnected = await this.midiService.connectToDevice(master.deviceId);

}

build() {
Column() {
Text(this.deviceName)
.fontSize(24)
.fontWeight(FontWeight.Bold)
.margin({bottom: 30})

  Image(this.isConnected ? 'resources/midi_connected.png' : 'resources/midi_disconnected.png')
    .width(120)
    .height(120)
    .margin({bottom: 20})
  
  Text(this.isConnected ? '已连接到主控设备' : '等待连接...')
    .fontSize(18)
    .margin({bottom: 30})
  
  if (!this.isConnected) {
    Button('连接到主控')
      .width(200)
      .height(50)
      .fontSize(18)
      .onClick(() => this.connectToMaster())

Text(‘连接后即可开始演奏’)

    .fontSize(14)
    .opacity(0.7)
    .margin({top: 30})

.width(‘100%’)

.height('100%')
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
.backgroundColor('#212121')

}

四、与游戏同步技术的结合点
实时数据同步机制:借鉴游戏中玩家动作同步技术,实现MIDI音符的实时同步传输

分布式设备发现:使用游戏中的设备发现机制自动组建音乐协作网络

角色分工:主控设备与从属设备的分工,类似游戏中的主机与客户端

状态同步:保持各设备的播放状态同步,类似游戏中的游戏状态同步

低延迟优化:应用游戏中的网络延迟优化策略,确保音乐同步精度

五、关键特性实现
MIDI协议优化:

  // MIDI消息压缩传输

compressMidiMessage(message: MidiMessage): Uint8Array {
const bytes = new Uint8Array(3);
bytes[0] = message.type | (message.channel & 0x0F);
bytes[1] = message.note || 0;
bytes[2] = message.velocity || 0;
return bytes;

时钟同步:

  // 同步播放时钟

syncPlaybackClock(masterTime: number): void {
const clockDiff = masterTime - Date.now();
this.playStartTime += clockDiff;

音频缓冲优化:

  // 动态调整音频缓冲区

adjustBufferSize(latency: number): void {
const targetSize = Math.floor(this.sampleRate * latency / 1000);
this.audioRenderer?.setBufferSize(targetSize);

设备能力适配:

  // 根据设备性能调整合成质量

if (device.perfLevel === ‘low’) {
this.synthQuality = ‘basic’;
this.polyphony = 8;
else {

 this.synthQuality = 'high';
 this.polyphony = 32;

六、性能优化策略
MIDI消息批处理:

  private batchMessages: MidiMessage[] = [];

private batchTimer: number = 0;

addToBatch(message: MidiMessage): void {
this.batchMessages.push(message);
if (!this.batchTimer) {
this.batchTimer = setTimeout(() => {
this.sendBatch();
this.batchTimer = 0;
}, 10); // 每10ms批量发送一次
}

差分数据同步:

  // 只同步变化的轨道数据

if (JSON.stringify(track) !== this.lastTrackState.get(track.id)) {
this.syncService.syncMidiTrack(track);
this.lastTrackState.set(track.id, JSON.stringify(track));

设备端渲染优化:

  // 根据CPU负载调整音频质量

monitorCpuUsage(): void {
if (this.cpuUsage > 70) {
this.reduceVoiceCount();
}

蓝牙连接优化:

  // 自动重连机制

bluetooth.on(‘disconnect’, (deviceId) => {
setTimeout(() => this.connectDevice(deviceId), 1000);
});

七、项目扩展方向
乐谱编辑:增加可视化乐谱编辑功能

效果器链:集成多种音频效果器(混响、延迟等)

AI伴奏:基于AI生成伴奏轨道

多人在线协作:云端协同音乐创作

音乐可视化:实时音频可视化效果

八、总结

本蓝牙MIDI音乐合成器实现了以下核心功能:
多设备蓝牙MIDI连接与通信

实时MIDI音符传输与同步

多轨道音乐合成与混音

分布式播放控制

低延迟音频渲染

通过借鉴游戏中的多设备同步技术,我们构建了一个高效、实时的音乐协作系统。该项目展示了HarmonyOS分布式能力在音乐科技领域的创新应用,为开发者提供了实现多设备协同音乐应用的参考方案。

©著作权归作者所有,如需转载,请注明出处,否则将追究法律责任
收藏
回复
举报
回复
    相关推荐