iOS播放/渲染/解析MIDI

Signal:这个用React写的开源网页版MIDI编辑器,能替代你的桌面软件吗? Signal是一款基于React开发的开源网页版MIDI编辑器,旨在挑战传统桌面音乐制作软件。通过Web Audio API和Web MIDI API技术栈,Signal提供了钢琴卷帘界面、实时协作潜力和设备无关性等核心功能。虽然在某些性能指标上略逊于专业软件如FL Studio,但其零成本入门和跨平台一致性使其成为音乐制作的新选择。 阅读详情

什么是MIDI

MIDI:乐器数字接口, Musical Instrument Digital Interface。
MIDI 是计算机能理解的乐谱,计算机和电子乐器都可以处理的乐器格式。
MIDI 不是音频信号,不包含 pcm buffer。
通过音序器 sequencer,结合音频数据 / 乐器 ,播放 MIDI Event 数据
( 通过音色库 SoundFont,播放乐器的声音。iOS上一般称sound bank )。

通过 AVAudioEngine/AVAudioSequencer 播放

连接 AVAudioEngine 的输入和输出,
输入 AVAudioUnitMIDIInstrument → 混频器 engine.mainMixerNode → 输出 engine.outputNode
用AVAudioEngine ,创建 AVAudioSequencer ,就可以播放 MIDI 了。
播放MIDI

配置 AVAudioEngine 的输入输出

var engine = AVAudioEngine()
var sampler = AVAudioUnitSampler() // AVAudioUnitMIDIInstrument的子类
engine.attach(sampler)
// 节点 node 的 bus 0 是输出,
// bus 1 是输入
let outputHWFormat = engine.outputNode.outputFormat(forBus: 0)
engine.connect(sampler, to: engine.mainMixerNode, format: outputHWFormat)

guard let bankURL = Bundle.main.url(forResource: soundFontMuseCoreName, withExtension: "sf2") else {
    fatalError("\(self.soundFontMuseCoreName).sf2 file not found.")
}
// 载入资源
do {
    try
        self.sampler.loadSoundBankInstrument(at: bankURL,
            program: 0,
            bankMSB: UInt8(kAUSampler_DefaultMelodicBankMSB),
            bankLSB: UInt8(kAUSampler_DefaultBankLSB))
    
    try engine.start()
} catch {  print(error)  }

engine.mainMixerNode是AVAudioEngine自带的node。负责混音,它有多路输入,一路输出。

用 AVAudioSequencer ,播放 MIDI

AVAudioSequencer 可以用不同的音频轨道 track,对应不同的乐器声音
tracks[index] 指向不同的音频产生节点

var sequencer = AVAudioSequencer(audioEngine: engine)
guard let fileURL = Bundle.main.url(forResource: "sibeliusGMajor", withExtension: "mid") else {
    fatalError("\"sibeliusGMajor.mid\" file not found.")
}

do {
    try sequencer.load(from: fileURL, options: .smfChannelsToTracks)
    print("loaded \(fileURL)")
} catch {
    fatalError("something screwed up while loading midi file \n \(error)")
}
// 指定每个track的dest AudioUnit
for track in sequencer.tracks {
    track.destinationAudioUnit = self.sampler
}

sequencer.prepareToPlay()
do {
    try sequencer.start()
} catch {
    print("\(error)")
}

加载多个音色库

有这样一种case,随着业务发展,音色库需要有更新,增加新的乐器,如果每次更新都要全量更新音色库,流量消耗大。所以需要对音色库做增量更新。这样就会在客户端出现多个音色库,看前面的代码,每次播放只能加载一个音色库。那么有没有什么办法可以加载多个音色库播放一个MIDI文件呢?
答案是可以的。
我们看前面的数据流图,实际上,AudioEngine的mainMixerNode是有多路输入的,那么它应该可以连接多个输入Instrument。代码类似下面:

engine.connect(midiSynth0, to: engine.mainMixerNode, format: nil)
engine.connect(midiSynth1, to: engine.mainMixerNode, format: nil)
engine.connect(midiSynth2, to: engine.mainMixerNode, format: nil)
engine.connect(midiSynth3, to: engine.mainMixerNode, format: nil)
engine.connect(midiSynth4, to: engine.mainMixerNode, format: nil)
engine.connect(engine.mainMixerNode, to: engine.outputNode, format: nil)    

  

这里,假定midiSynth0~midiSynth4是五个不同的AVAudioUnitSampler实例,他们分别加载不同的音色库,假定为soundBank 0~4用他们来播放一个MIDI文件。我们期望,可以正常播放出MIDI中描述的所有轨道的音色。但是实际测试发现,只有midiSynth0挂载的音色库里的音色被渲染了出来。奇怪,这是为什么呢?
实际上,答案就隐藏在之前的数据流图上。这里,我们虽然创建了多个AVAudioUnitSampler作为input node,但是,因为我们没有指定MIDI每一个track的destinationAudioUnit,MIDI默认所有的track都通过midiSynth0,而midiSynth0只挂载了soundBank0的音色,所以,只有soundBank0被渲染了出来。
那么,这个问题怎么解决呢?其实,之前的代码里,我们已经给出了答案,即这一句:

for track in sequencer.tracks {
    track.destinationAudioUnit = self.sampler
}

指定每一个track的destinationAudioUnit为对应的挂载了改track上instrument音色的音色库的AVAudioUnitSampler,让对应的track上的event通过对应AudioUnitSampler,则可以渲染出完整的音色。

MIDI文件渲染成音频文件

我们可以把MIDI文件渲染为wav/caf等音频文件,这需要开启AVAudioEngine的离线渲染模式。代码如下:

/**
  * 渲染midi到音频文件(wav格式)。耗时操作。
  * @param midiPath 要渲染的midi文件路径
  * @param audioPath 输出的音频文件路径
  */
 public func render(midiPath: String, audioPath: String) {
     
     let renderEngine = AVAudioEngine()
     
     let renderMidiSynth = AVAudioUnitMIDISynth()
     
     loadSoundFont(midiSynth: renderMidiSynth, path:soundFontPath)
     
     renderEngine.attach(renderMidiSynth)
     renderEngine.connect(renderMidiSynth, to: renderEngine.mainMixerNode, format: nil)
     
     let renderSequencer = AVAudioSequencer(audioEngine: renderEngine)
     
     if renderSequencer.isPlaying {
         renderSequencer.stop()
     }
     
     renderSequencer.currentPositionInBeats = TimeInterval(0)
 
     do {
         let format = AVAudioFormat(standardFormatWithSampleRate: 44100, channels: 2)!
         
         do {
             let maxFrames: AVAudioFrameCount = 4096
             if #available(iOS 11.0, *) {
                 try renderEngine.enableManualRenderingMode(.offline, format: format,
                                                      maximumFrameCount: maxFrames)
             } else {
                 fatalError("Enabling manual rendering mode failed")
             }
         } catch {
             fatalError("Enabling manual rendering mode failed: \(error).")
         }
         
         if (!renderEngine.isRunning) {
             do {
                 try renderEngine.start()
             } catch {
                 print("start render engine failed")
             }
         }
         
         setupSequencerFile(sequencer: renderSequencer, midiPath:midiPath)
         
         print("attempting to play")
         do {
             try renderSequencer.start()
             print("playing")
         } catch {
             print("cannot start \(error)")
         }
         
         if #available(iOS 11.0, *) {
             // The output buffer to which the engine renders the processed data.
             let buffer = AVAudioPCMBuffer(pcmFormat: renderEngine.manualRenderingFormat,
                                           frameCapacity: renderEngine.manualRenderingMaximumFrameCount)!
             let avChannelLayoutKey: [UInt8] = [0x02, 0x00, 0x65, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
             let avChannelLayoutData = NSData.init(bytes: avChannelLayoutKey, length: 12)
             
             let settings: [String: Any] = ["AVLinearPCMIsFloatKey": 0, "AVFormatIDKey":1819304813, "AVSampleRateKey": 44100, "AVLinearPCMBitDepthKey": 16,
                                            "AVLinearPCMIsNonInterleaved": 0, "AVLinearPCMIsBigEndianKey": 1, "AVChannelLayoutKey": avChannelLayoutData, "AVNumberOfChannelsKey": 2]
             
             let outputFile: AVAudioFile
             do {
                 let outputURL = URL(fileURLWithPath: audioPath)
                 outputFile = try AVAudioFile(forWriting: outputURL, settings: settings)
             } catch {
                 fatalError("Unable to open output audio file: \(error).")
             }
             
             var totalFrames: Int = 0;
             for track in renderSequencer.tracks {
                 let trackFrames = track.lengthInSeconds * 1000 * 44100 / (1024 / 1.0)
                 if (Int(trackFrames) > totalFrames) {
                     totalFrames = Int(trackFrames)
                 }
             }
             
             let totalFramesCount = AVAudioFramePosition(totalFrames)
             
             while renderEngine.manualRenderingSampleTime < totalFrames {
                 do {
                     let frameCount = totalFramesCount - renderEngine.manualRenderingSampleTime
                     let framesToRender = min(AVAudioFrameCount(frameCount), buffer.frameCapacity)
                     
                     let status = try renderEngine.renderOffline(framesToRender, to: buffer)
                     
                     switch status {
                     case .success:
                         try outputFile.write(from: buffer)
                     case .insufficientDataFromInputNode:
                         break
                     case .cannotDoInCurrentContext:
                         break
                     case .error:
                         fatalError("The manual rendering failed.")
                     }
                 } catch {
                     fatalError("The manual rendering failed: \(error).")
                 }
             }
             
             print("isPlaying 2: " + String(renderSequencer.isPlaying) + ", " + String(renderSequencer.currentPositionInBeats) + ", " + String(renderSequencer.currentPositionInSeconds))
         } else {
             // Fallback on earlier versions
         }
     }
     renderEngine.stop()
 }

渲染的流程其实和播放完全一致,只是需要为渲染单独创建AVAudioEngine和AVAudioSequencer的实例。通过AVAudioEngine的enableManualRenderingMode开启离线渲染模式。代码中的settings是一个字典,存储输出文件相关的参数。其中各个参数的具体含义可以在苹果开发者网站查到了。比如AVSampleRateKey表示采样率,AVLinearPCMBitDepthKey表示采样深度。
循环遍历MIDI的所有track,计算出总帧数。秒数与帧的换算公式track.lengthInSeconds * 1000 * 44100 / (1024 / 1.0)。然后,循环输出所有帧到音频文件。

此外,还可以解析MIDI文件,获取每个track,以及每个track对应的,编辑MIDI文件,重新生成一个新的MIDI文件。
可以从这里获取源码:iOS MIDI播放

【Totoro】基于ESP32与蓝牙MIDI的仿古埙电子乐器DIY全解析 本文详细解析了如何利用ESP32主控芯片与蓝牙MIDI技术,DIY一款名为Totoro的仿古埙电子乐器。项目通过气流检测电路感知吹奏力度,结合指法按键控制音符,并借助手机APP实现无线音色合成与播放,同时集成了WS2812灯光效果,完整涵盖了从硬件设计、电路分析到软件烧录与演奏连接的全过程。 阅读详情

相关推荐

iOS 音频项目大全之iOS复调合成器支持AUV3和SF2 可连接蓝牙Bluetooth MIDI(教程含源码)

实战需求 这是一个简单而强大的应用程序,可以作为复调音频合成器,具有几乎无限的声音集合。 该应用程序支持MIDI输入——只需将MIDI键盘/控制器连接到您的iOS设备,您应该就可以开始了!还支持通过蓝牙进行 MIDI。 支持功能 支持 iCloud云盘下载SF2文件。 通过触摸波浪形“信号”按钮来调出混响和延迟效果。 在补丁名称上向右轻扫,使其成为收藏夹。最喜欢的补丁在它们的名字旁边有一个星星。您可以通过再次向右滑动并选择编辑操作来编辑收藏夹的名称。您可以通过向左滑动并选择删除操作来删除收藏夹。 触摸黑条

iCloudEnd的博客 922

iOS8开发技术(Swift版):音频和视频

包括播放系统声音,AVAudioPlayer,AVAUdioEngine、恢复中断背景音乐、AVKit等。

用HTML5编写乐谱

本文将告诉您如何使用纯JavaScript编写音乐,并向您介绍一个有趣的Javascript框架。

简谱渲染引擎-Web

这是一款功能强大的简谱智能渲染与播放引擎,支持MIDI旋律播放、多轨伴奏协奏、实时调速调高等11项核心功能。系统提供横屏演奏视图,支持谱面缩放自如、自动小节线划分,兼容jp-word格式文件,并内置节拍器模块。无论是教学、创作还是演奏,都能获得专业级体验。项目采用响应式设计,适配各类设备,点击链接即可查看项目详情。

Date_Boy的博客 810

34款经典游戏音乐音效MIDI

34款经典游戏音乐音效MIDI 游戏 印象

MidiFilePlayer:在iOS上播放.mid的Phonegap插件

MidiFilePlayer Phonegap插件,可使用soundfont在iOS上播放Midi文件 如果您需要帮助以了解如何使用它,请参考示例目录 现在可以在Android和iO上使用。

ios android mid音频文件,ios – 使用MusicPlayer和Music Sequence播放midi文件

我使用以下代码成功地让iOS使用soundfont示例播放.mid(midi)文件:-(void) playMusic:(NSString*) name{NSString *presetURLPath = [[NSBundle mainBundle] pathForResource:@"GortsMiniPianoJ1" ofType:@"SF2"];NSURL * presetURL = [NS...

weixin_33826897的博客 961

SwiftAudioPlayer:使用AVAudioEngine进行流式传输和实时音频处理

SwiftAudioPlayer 基于Swift的音频播放器,以AVAudioEngine为基础。 允许:流式传输在线音频,播放本地文件,更改音频速度(3.5倍,4倍,32倍),音调和使用自定义的实时音频处理。 该播放器是为。 我们最初使用AVPlayer播放音频,但我们想操纵流式传输的音频。 我们最初设置AVAudioEngine只是为了播放保存在手机上的文件,并且效果很好,但是AVAudioEngine本身不像AVPlayer那样容易地支持流音频。 因此,使用 ,我们可以流音频并将下载的数据转换为可用数据,以供AVAudioEngine播放。 有关我们的解决方案的概述,请查看我们的。 基本特征 实时音频操作,包括使用,最高可达到10倍的速度 使用AVAudioEngine流式传输在线音频 使用相同的API播放本地保存的音频 下载音频 排队下载和流式传输音频以进行自动播放 仅使用1

我如何用 JavaScript 开发一个简谱渲染库

在音乐教育和数字乐谱领域,简谱(数字谱)依然拥有广泛的用户群体。相比五线谱,简谱更易于学习和传播。然而,市面上针对简谱的渲染工具却寥寥无几。作为一名前端开发者兼音乐爱好者,我决定用 JavaScript 开发一个简谱渲染库,让简谱在网页上也能优雅呈现。

Date_Boy的博客 621

iOS mid 格式文件播放 demo

发现网上关于iOS版mid格式音频播放的轮子很少;只找到一个大神写的很久的swift版的,小弟由于项目的原因。从他的基础上做了一些调整,特意弄了一个oc版的demo,希望能够给遇到的此类问题的iOSers一些帮助

从零开始:Basler相机与pylon Viewer的工业视觉入门指南

本文详细介绍了Basler相机与pylon Viewer在工业视觉系统中的入门指南,涵盖硬件连接、软件安装、核心功能解析及实战案例。通过pylon Viewer的全面控制和参数调整功能,帮助新手快速掌握工业视觉检测技术,提升生产效率。

julia4scientist的博客 1136

iOS MIDI播放渲染解析实践:从AudioToolbox到钢琴卷帘

MIDI本质上是由音符事件与时间戳组成的指令序列,在iOS开发中常涉及播放、解析和渲染三类任务。通过AudioToolbox框架中的MusicSequence与MusicPlayer,开发者可以读取MIDI文件并精确控制播放进度;CoreMIDI则负责外部设备的实时通信。理解tick、beat与秒之间的换算关系,以及TempoMap的构建,是准确渲染钢琴卷帘并同步播放头的关键。这类技术适用于音乐教育、编曲练琴及定制播放器等场景。从API选型到解析建模,再到UI绘制优化,本文梳理了一个完整的实践路线,帮助iO

weixin_29014939的博客 191

单片机通信总述——理论部分(CAN、串口、SPI、I2C等)

并行通信:传输原理:数据各个位同时传输;优点:速度快; 缺点:占用引脚资源多。是指使用 8、16、32 及 64 根或更多的数据线(有多少信号为就需要多少信号位)进行传输的通讯方式,可以同一时刻传输多个数据位的数据。串行通信: 传输原理:数据按位顺序传输;优点:占用引脚资源少; 缺点:速度相对较慢。是指设备之间通过一根数据信号线,地线以及控制信号线,按数据位形式一位一位地传输数据的通讯方式,同一时刻只能传输一位(bit)数据。 单工: 数据传输只支持数据在一个方向上传输。信息只能单方向传输的工作方式,一

qq_42475191的博客 1万+

Black MIDI实时播放器开发:从MIDI解析到多线程渲染优化

MIDI(Musical Instrument Digital Interface)作为数字音乐的标准协议,其核心原理是通过事件序列描述音乐演奏信息。在实时音频处理领域,MIDI技术价值在于能够高效传输音乐控制数据,广泛应用于音乐制作、游戏音效和交互式媒体等场景。当面对Black MIDI这类超高密度音乐文件时,传统音序器往往性能不足,需要深入优化多线程渲染和实时调度机制。本文基于RtAudio、RtMidi和Lumino框架,详细解析如何构建高性能Black MIDI播放器,涵盖MIDI文件解析、音频合成

weixin_34081595的博客 432

Ubuntu运行vmware时报错Vmware Unable to install all modules!Use shipped Linux kernel AIO access library解决。

Ubuntu运行vmware时报错Vmware Unable to install all modules! vmware --version 提示下面内容: [AppLoader] Use shipped Linux kernel AIO access library. An up-to-date "libaio" or "libaio1" package from your system is preferred. VMware Workstation 16.1.2 build-17966106

wto882dim的博客 2848

[官方培训]07-UE材质基础 孙丹璐 Epic 笔记

勾选允许自定义深度写入再在模型上勾选然后可以通过Scene Texture节点拿到渲染的自定义深度信息,有了自定义深度信息就可以与当前渲染的像素深度进行对比,自定义深度信息存放的是最靠近我们的深度,如果当前像素离自定义深度比较远我们就不要这个像素。效果如下这种方案的缺点也很明显,因为这样看不到第二层的透明了,如果是两层的半透明的裙摆,这样就看不到第二层的裙摆了。对于半透明排序问题最根本能解决的还是光线追踪,ue5中又加入了OIT(基于像素的排序)。官方透明材质的理解和应用。

Tcohneyn的博客 1562

iOS MIDI解析播放渲染全链路实践:从.mid到音符可视化

在数字音频工程中,MIDI常被误认为是一种音频格式,其实它是一串精确的演奏指令。理解MIDI的本质,需要先掌握文件结构、时间编码与事件语义,再结合系统级音频单元完成声音合成,最终通过图形化方式呈现音乐数据。这种“指令—声音—视觉”的完整链路在音乐App、跟弹教学、自动伴奏和音频可视化等场景中具有很高的工程价值。基于Core Audio的AVAudioUnitSampler,结合自研解析器与Core Graphics渲染,可以在无第三方依赖的前提下实现从.mid文件解析、音符调度、实时播放到钢琴卷帘绘制以及离

weixin_31115195的博客 131

技术指南| 叶绿体基因组高级分析全流程解析

本文详细解析了叶绿体基因组高级分析的全流程,包括实验设计、序列组装与注释、比较基因组分析、选择压力与功能进化等关键步骤。通过实战案例和技术技巧,帮助研究者掌握叶绿体基因组分析的核心方法,提升植物进化研究和系统学分析的效率与准确性。

weixin_33725239的博客 491

AudioKit 集成指南:从 Xcode 安装到 iOS/macOS/tvOS 音频开发实战

AudioKit 是一款面向 iOS、macOS(含 Catalyst)与 tvOS 的音频合成、处理与分析平台,本仓库即其 Swift 基础层源码。本文以官方 README 的安装与使用流程为主线,结合仓库源码、Swift Package 配置与测试用例,系统讲解如何在 Xcode 中通过 Package Collection 引入 AudioKit、构建音频信号链、完成格式转换、MIDI 与音

gitblog_01148的博客 1015

【GameFramework框架内置模块】15、场景(Scene)

场景(Scene)模块,提供场景管理的功能,负责场景的加载、卸载、切换和管理。可以方便地管理不同场景之间的切换以及不同场景之间的信息传递。

优快云博客专家,Unity3D高级软件工程师,《Unity 3D从入门到实战》作者。 1887

基于QT+Halcon实现线序识别

一、线序识别算法步骤。 1.按照检测区域m_pItem1的外接矩形(rtBount)的大小, 截取感兴趣区域图像hRoi。 2.把截取出来的感兴趣区域图像hRoi, 分离出R、G、B三个通道hRed、hGreen、hBlue。 3.把hRed、hGreen、hBlue三个通道转换成cielab颜色空间, 得到hChannelL、hChannelA、hChannelB三个颜色图。 4.设m_pItem1的宽为iItemW, 高为iItemH. 分别申请大小为iItemW的pSumL、pSumA、pSumB内

专注现代C++实战,从核心语法到高性能编程,助力开发者深入掌握系统级编程精髓。 1748

Unity游戏开发实战:跨平台MIDI播放与动态音乐生成方案

MIDI(Musical Instrument Digital Interface)作为一种数字音乐指令协议,其核心原理是通过记录音符、力度、时长等乐谱信息而非音频波形,实现了音乐数据的极致压缩与灵活控制。在游戏开发与交互式音频领域,这项技术的价值在于支持运行时动态生成与修改音乐,能够基于游戏逻辑实时驱动音乐演进,极大提升了资源的利用效率与音频的交互性。常见的应用场景包括音乐节奏游戏、RPG动态配乐以及模拟经营类游戏的智能背景音乐适配。本文聚焦于Unity引擎,探讨如何通过集成CSharpSynth合成器库

tyygming的专栏 130

<PLC><工控>汇川Easy521PLC与压力传感器485通讯实例

有些PLC在进行串口通讯时,配置完串口参数后,还需要自己编程序来读写,但本例中汇川Easy521配置完成后,想要读取从站什么数据,只需要在“COM0 Modbus配置窗口”配置即可。可以同时配置多个从站。在配置窗口的右侧点击新增按钮,就会自动生成一条从站的配置,默认从站站号为“1”,触发方式是“循环”,功能码是03(读寄存器),从站寄存器地址为0,长度为“1”,映射地址需要自己添加。因为我们是将PLC设置为Modbus主站,所以压力传感器就是从站,我们的需求时PLC从这些从站传感器中读取数据,比如压力值。

用沸腾的热血,支付我们的人生吧! 6458

探索音乐新领域:Flutter_piano —— 跨平台MIDI钢琴应用

探索音乐新领域:Flutter_piano —— 跨平台MIDI钢琴应用 flutter_pianoA Cross Platform Piano made with Flutter项目地址:https://gitcode.com/gh_mirrors/fl/flutter_piano Flutter_piano 是一款由Flutter框架构建的全平台MIDI钢琴应用,将为你带来全新的演奏体验。它...

gitblog_00100的博客 657

win10 跳过U8环境检测的IE WebControls.rar

win10 安装u8的时候 提示IE WebControls 不通过,这个工具可以欺骗u8的检查。 跳过U8环境检测的IE WebControls。提示导入失败后,重新检测通过。

假人有限元模型

假人模型,车辆碰撞假人有限元模型,适合做乘员安全的研究者利用

TiXL MIDI 集成实战指南:Lib.io.midi 操作符库的输入、输出与时钟同步全解析

TiXL(开源实时动态图形软件)通过 `Lib.io.midi` 操作符库提供了一套完整的 MIDI 集成能力:既能接收实体或虚拟 MIDI 设备的控制信号并映射为图形参数,也能向外部设备输出音符、控制变化(CC)、弯音、SysEx 与走带命令,还能以 MIDI 时钟驱动播放时间、以时间轴 Clip 方式加载 MIDI 文件,甚至录制 MIDI 输入。本文以该操作符库的官方文档([.help/do

gitblog_00743的博客 1038

老版QQ原生表情gif动态

这是老版的QQ表情,不过全是动态的的,Windows照片查看器是不会动的,可以右击用浏览器打开查看,再说一遍是老版的,只是分享给宁愿用老版也要动态的人。

AudioKit 音频开发平台深度指南:从 FormatConverter 到 MIDI、Tables 与 Sequencing 的完整实践

AudioKit 是面向 iOS、macOS(含 Catalyst)与 tvOS 的 Swift 音频合成、处理与分析平台,本文基于 AudioKit 的官方 Docc 文档([Sources/AudioKit/AudioKit.docc/AudioKit.md](https://link.gitcode.com/i/c6e43dac1ffb986ba18936e64191feb1))撰写,系统讲

gitblog_00206的博客 451

蓝牙模块HM-10手册

蓝牙模块HM-10手册,该手册对蓝牙模块HM-10进行了详细的介绍和使用方法以及一些AT指令。

上一篇: 【Android】AudioRecord::getMinBufferSize源码分析
下一篇: 客户端日志打印规范
hunter800421
博客等级 码龄22年 60粉丝 97原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值