
鸿蒙5蓝牙MIDI音乐合成器开发指南 原创
鸿蒙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: false,
backup: false,
autoSync: true,
kvStoreType: distributedData.KVStoreType.SINGLE_VERSION,
schema: '',
securityLevel: distributedData.SecurityLevel.S1
};
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 = 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分布式能力在音乐科技领域的创新应用,为开发者提供了实现多设备协同音乐应用的参考方案。
