当前位置: 首页 > news >正文

深入解读.NET MAUI音乐播放器项目(一):概述与架构

系列文章将分步解读音乐播放器核心业务及代码:

  • 深入解读.NET MAUI音乐播放器项目(一):概述与架构
  • 深入解读.NET MAUI音乐播放器项目(二):播放内核
  • 深入解读.NET MAUI音乐播放器项目(三):界面与交互

为什么想起来这个项目了呢?

这是一个Windows Phone 8的老项目,2014年用作为兴趣写了个叫“番茄播放器”的App,顺便提高编程技能。

这个项目的架构历经多次迁移,从WP8到UWP再到Xamarin.Forms。去年底随着MAUI的正式发布,又尝试把它迁移到MAUI上来。

虽然历经数次迁移,但命名空间和播放内核的代码基本没怎么改动,这个项目随着解决方案升级,依赖库、API调用方式的变更,见证了微软在移动互联网领域的动荡。我偶然发现8年前提交到微软商店的App,竟然还能够打开下载页面 - Microsoft应用商店,但由于我手边没有一台Windows Phone设备,也没法让它在任何的模拟器中跑起来。也只能从商店截图和源代码中重温这个物件和那段时光。

这个项目现在已经没有任何的商业价值,但我知道它对于我意味着什么,曾给我带来的在编程时的那种欣喜和享受,可以说真正让我知道什么叫“Code 4 Fun”——编程带来的快乐,对于那时刚进入社会的我,树立信心和坚持道路有莫大的帮助。

这个项目可能从来就没有价值。那么写博文和开源能发挥多少价值就算多少吧。

当下在.Net平台上有不少开源的音频封装库,如Plugin.Maui.Audio,本项目没有依赖任何音频的第三方库,希望大家以学习的态度交流,如果您有更好的实现方式,欢迎在文章下留言。因为代码年代久远且近年来没有重构,C#语言版本和代码写法上会有不少繁冗,这里还要向大家说声抱歉。

在这里插入图片描述

架构

使用Abp框架,我之前写过如何 将Abp移植进.NET MAUI项目,本项目也是按照这篇博文完成项目搭建。

跨平台

使用.NET MAU实现跨平台支持,从Xamarin.Forms移植的应用可以在Android和iOS平台上顺利运行。

播放内核是由分部类提供跨平台支持的,在Xamarin.Forms时代,需要维护不同平台的项目,MAUI是单个项目支持多个平台。
MAUI 应用项目包含 一个 Platform 文件夹,每个子文件夹表示 .NET MAUI 可以面向的平台

每个文件夹代表了每个平台特定的代码, 在默认的情况下 编译阶段仅仅会编译当前选择的平台文件夹代码。

这属于利用分部类和方法创建平台特定内容,详情请参考官方文档

IMusicControlService在项目中分部类实现:

MatoMusic.Core\Impl\MusicControlService.cs
MatoMusic.Core\Platforms\Android\MusicControlService.cs
MatoMusic.Core\Platforms\iOS\MusicControlService.cs
MatoMusic.Core\Platforms\Windows\MusicControlService.cs

核心类

在设计播放内核时,从用户的交互路径思考,抽象出了曲目管理器IMusicInfoManager和播放控制服务IMusicControlService

播放器行为和曲目操作行为在各自领域相互隔离,通过生产-消费模型,数据流转和消息通知冒泡协调一致。尽量规避了大规模使用线程锁,以及复杂的线程同步逻辑。在跨平台方案中,通过分部类实现了这些接口,类图如下:
在这里插入图片描述

音乐播放相关服务类MusicRelatedService是播放控制服务的一层封装,在实际播放器业务逻辑上,利用封装的代码能更方便的完成任务。

项目遵循MVVM设计模式,MusicRelatedViewModel作为音乐播放相关ViewModel的基类,包含了曲目管理器IMusicInfoManager和播放控制服务IMusicControlService对象,通过双向绑定开发者可以从表现层轻松进行音乐控制和曲目访问

ViewModelBase是个基础类,它继承自AbpServiceBase,封装了Abp框架通用功能的调用。比如Setting、Localization和UnitOfWork功能。并且实现了INotifyPropertyChanged,它为绑定类型的每个属性提供变更事件。

核心类图如下
在这里插入图片描述

定义

  • Queue - 歌曲队列,当前用于播放歌曲的有序列表
  • Playlist - 歌单,存储可播放内容的集合,用于收藏曲目,添加到我的最爱等。
  • PlaylistEntry - 歌单条目,可播放内容,关联一个本地音乐或在线音乐信息
  • MyFavourite - 我的最爱,一个id为0的特殊的歌单,不可编辑和删除,用于记录点亮歌曲小红心
  • MusicInfo - 曲目信息
  • AlbumInfo - 专辑信息
  • ArtistInfo - 艺术家信息
  • BillboardInfo - 排行榜,在线音乐歌单

曲目

曲目包含:

  • Title - 音乐标题
  • AlbumTitle - 专辑标题
  • GroupHeader - 标题头,用于列表分组显示的依据
  • Url - 音频文件地址
  • Artist - 艺术家
  • Genre - 流派
  • IsFavourite - 是否已“我最喜爱”
  • IsPlaying - 是否正在播放
  • AlbumArtPath - 封面图片
  • Duration - 歌曲总时长

如果配合模糊搜索控件,需要实现IClueObject,使用方式请参考AutoComplete控件

public class MusicInfo : ObservableObject, IBasicInfo, IClueObject
{ .. }
public List<string> ClueStrings
{get{var result = new List<string>();result.Add(Title);result.Add(Artist);result.Add(AlbumTitle);return result;}
}

它继承自ObservableObject,构造函数中注册属性更改事件
IsFavourite更改时,将调用MusicInfoManager将当前曲目设为或取消设为“我最喜爱”

private void MusicInfo_PropertyChanged(object sender, PropertyChangedEventArgs e)
{var MusicInfoManager = IocManager.Instance.Resolve<MusicInfoManager>();if (e.PropertyName == nameof(IsFavourite)){if (IsFavourite){MusicInfoManager.CreatePlaylistEntryToMyFavourite(this);}else{MusicInfoManager.DeletePlaylistEntryFromMyFavourite(this);}}
}

曲目集合

曲目集合是歌单,音乐专辑或者艺术家(演唱者)创作的音乐的抽象,它包含:

  • Title - 标题,歌单,音乐专辑或者艺术家名称
  • GroupHeader - 标题头,用于列表分组显示的依据
  • Musics - 曲目信息集合
  • AlbumArtPath - 封面图片
  • Count - 歌曲集合曲目数
  • Time - 歌曲集合总时长

它继承自ObservableObject

AlbumInfoArtistInfoPlaylistInfoBillboardInfo 都是曲目集合的子类
在这里插入图片描述

Musics是曲目集合的内容,类型为ObservableCollection<MusicInfo>,双向绑定时提供队列变更事件。

集合曲目数和集合总时长依赖这个变量

public int Count => Musics.Count();
public string Time
{get{var totalSec = Math.Truncate((double)Musics.Sum(c => (long)c.Duration));var totalTime = TimeSpan.FromSeconds(totalSec);var time = totalTime.ToString("g");return time;}
}

当集合内容增删时,同步通知歌曲集合曲目数以及总时长变更

private void _musics_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{if (e.Action == NotifyCollectionChangedAction.Remove || e.Action == NotifyCollectionChangedAction.Add){RaisePropertyChanged(nameof(Time));RaisePropertyChanged(nameof(Count));}
}

GroupHeader标题头,一般取得是标题的首字母,若标题为中文,则使用Microsoft.International.Converters.PinYinConverter获取中文第一个字的拼音首字母,跨平台实现方式如下:

private partial string GetGroupHeader(string title)
{string result = string.Empty;if (!string.IsNullOrEmpty(title)){if (Regex.IsMatch(title.Substring(0, 1), @"^[\u4e00-\u9fa5]+$")){try{var chinese = new ChineseChar(title.First());result = chinese.Pinyins[0].Substring(0, 1);}catch (Exception ex){return string.Empty;}}else{result = title.Substring(0, 1);}}return result;}

GroupHeader用于列表分组显示的内容将在后续文章中阐述

数据库

应用程序里使用Sqlite,作为播放列表,歌单,设置等数据的持久化
,使用CodeFirst方式用EF初始化Sqlite数据库文件:mato.db

在MatoMusic.Core项目的appsettings.json中添加本地sqlite连接字符串

  "ConnectionStrings": {"Default": "Data Source=file:{0};"},...

这里文件是一个占位符,通过代码hardcode到配置文件

在MatoMusicCoreModule.cs中,重写PreInitialize并设置Configuration.DefaultNameOrConnectionString:

public override void PreInitialize()
{LocalizationConfigurer.Configure(Configuration.Localization);Configuration.Settings.Providers.Add<CommonSettingProvider>();string documentsPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), MatoMusicConsts.LocalizationSourceName);var configuration = AppConfigurations.Get(documentsPath, development);var connectionString = configuration.GetConnectionString(MatoMusicConsts.ConnectionStringName);var dbName = "mato.db";string dbPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), MatoMusicConsts.LocalizationSourceName, dbName);Configuration.DefaultNameOrConnectionString = String.Format(connectionString, dbPath);base.PreInitialize();
}

接下来定义实体类

播放队列

定义于\MatoMusic.Core\Models\Entities\Queue.cs

public class Queue : FullAuditedEntity<long>
{[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]public override long Id { get; set; }public long MusicInfoId { get; set; }public int Rank { get; set; }public string MusicTitle { get; set; }
}

歌单

定义于\MatoMusic.Core\Models\Entities\Playlist.cs

public class Playlist : FullAuditedEntity<long>
{[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]public override long Id { get; set; }public string Title { get; set; }public bool IsHidden { get; set; }public bool IsRemovable { get; set; }public ICollection<PlaylistItem> PlaylistItems { get; set; }
}

歌单条目

定义于\MatoMusic.Core\Models\Entities\PlaylistItem.cs

public class PlaylistItem : FullAuditedEntity<long>
{[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]public override long Id { get; set; }public int Rank { get; set; }public long PlaylistId { get; set; }[ForeignKey("PlaylistId")]public Playlist Playlist { get; set; }public string MusicTitle { get; set; }public long MusicInfoId { get; set; }
}

配置

数据库上下文对象MatoMusicDbContext定义如下

public class MatoMusicDbContext : AbpDbContext
{//Add DbSet properties for your entities...public DbSet<Queue> Queue { get; set; }public DbSet<Playlist> Playlist { get; set; }public DbSet<PlaylistItem> PlaylistItem { get; set; }...

MatoMusic.EntityFrameworkCore是应用程序数据库的维护和管理项目,依赖于Abp.EntityFrameworkCore。
在MatoMusic.EntityFrameworkCore项目中csproj文件中,引用下列包

<PackageReference Include="Abp.EntityFrameworkCore" Version="7.4.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Design" Version="1.1.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.0">

在该项目MatoMusicEntityFrameworkCoreModule.cs 中,将注册上下文对象,并在程序初始化运行迁移,此时将在设备上生成mato.db文件

public override void PostInitialize()
{Helper.WithDbContextHelper.WithDbContext<MatoMusicDbContext>(IocManager, RunMigrate);if (!SkipDbSeed){SeedHelper.SeedHostDb(IocManager);}
}public static void RunMigrate(MatoMusicDbContext dbContext)
{dbContext.Database.Migrate();
}

项目地址

GitHub:MatoMusic

下一章将介绍播放器核心功能:播放服务类

相关文章:

深入解读.NET MAUI音乐播放器项目(一):概述与架构

系列文章将分步解读音乐播放器核心业务及代码&#xff1a; 深入解读.NET MAUI音乐播放器项目&#xff08;一&#xff09;&#xff1a;概述与架构深入解读.NET MAUI音乐播放器项目&#xff08;二&#xff09;&#xff1a;播放内核深入解读.NET MAUI音乐播放器项目&#xff08;三…...

【Python小游戏】某程序员将套圈游戏玩儿到了巅峰,好嗨哟~Pygame代码版《牛牛套圈》已上线,大人的套圈游戏太嗨了,小孩勿进。

前言 世上选择那么多。 关注栗子同学会是您最明智的选择哦。 所有文章完整的素材源码都在&#x1f447;&#x1f447; 粉丝白嫖源码福利&#xff0c;请移步至CSDN社区或文末公众hao即可免费。 “幸运牛牛套圈圈”套住欢乐&#xff0c;圈住幸福&#xff0c;等你来挑战&#xf…...

php的declare命令如何使用?

php中的declare结构用来设定一段代码的执行指令declare用于执行3个指令&#xff1a;ticks,encoding,strict_typesdeclare结构用于全局范围&#xff0c;影响到其后的所有代码&#xff08;但如果有declare结构的文件被其他文件包含&#xff0c;则对包含他的父文件不起作用&#x…...

嵌软工程师要掌握的硬件知识2:一文看懂什么开漏和推挽电路(open-drain / push-pull)

想了解开漏和推挽,就要先了解一下三极管和场效应管是什么,在其他章节有详细介绍,本文就不再进行赘述。 1 推挽(push pull)电路 1.1 理解什么是推挽电路 - 详细介绍 如图所示,Q3是个NPN型三极管,Q4是个PNP型三极管。 1)当Vin电压为正时,上面的N型三极管控制端有电…...

1.2.6存储结构-磁盘管理:单缓冲区与双缓冲区读取、流水线周期、计算流水线执行时间

1.2.6存储结构-磁盘管理&#xff1a;单缓冲区与双缓冲区读取、流水线周期、计算流水线执行时间流水线周期计算流水线执行时间微秒&#xff0c;时间单位&#xff0c;符号μs&#xff08;英语&#xff1a;microsecond &#xff09;&#xff0c;1微秒等于百万分之一秒&#xff08;…...

【pytest接口自动化测试】结合单元测试框架pytest+数据驱动模型+allure

api&#xff1a; 存储测试接口 conftest.py :设置前置操作 目前前置操作&#xff1a;1、获取token并传入headers&#xff0c;2、获取命令行参数给到环境变量,指定运行环境commmon&#xff1a;存储封装的公共方法 connect_mysql.py&#xff1a;连接数据库http_requests.py: 封装…...

展锐平台WIFI吞吐问题解决方案

同学,别退出呀,我可是全网最牛逼的 WIFI/BT/GPS/NFC分析博主,我写了上百篇文章,请点击下面了解本专栏,进入本博主主页看看再走呗,一定不会让你后悔的,记得一定要去看主页置顶文章哦。 一、Wi-Fi 吞吐验收标准 预置条件:屏蔽房;DUT 距离 AP 1m 左右;测试 AP 不加密;…...

全局向量的词嵌入(GloVe)

诸如词-词共现计数的全局语料库统计可以来解释跳元模型。 交叉熵损失可能不是衡量两种概率分布差异的好选择&#xff0c;特别是对于大型语料库。GloVe使用平方损失来拟合预先计算的全局语料库统计数据。 对于GloVe中的任意词&#xff0c;中心词向量和上下文词向量在数学上是等…...

华为OD机试 - 特异性双端队列(Python),真题含思路

特异性双端队列 题目 有一个特异性的双端队列,该队列可以从头部到尾部添加数据,但是只能从头部移除数据。 小 A 一次执行 2 n 2n 2n 个指令往队列中添加数据和移除数据, 其中 n n n 个指令是添加数据(可能从头部也可以从尾部添加) 依次添加 1 到...

【Linux】操作系统进程概念

文章目录1. 冯诺依曼体系结构2. 操作系统3. 进程进程的基本概念查看进程和杀死进程父进程和子进程通过系统调用创建子进程1. 冯诺依曼体系结构 冯诺依曼结构也称普林斯顿结构&#xff0c;是一种将程序指令存储器和数据存储器合并在一起的存储器结构。数学家冯诺依曼提出了计算…...

C语言const的用法详解

有时候我们希望定义这样一种变量&#xff0c;它的值不能被改变&#xff0c;在整个作用域中都保持固定。例如&#xff0c;用一个变量来表示班级的最大人数&#xff0c;或者表示缓冲区的大小。为了满足这一要求&#xff0c;可以使用const关键字对变量加以限定&#xff1a;constin…...

Day886.MySQL的“饮鸩止渴”提高性能的方法 -MySQL实战

MySQL的“饮鸩止渴”提高性能的方法 HI&#xff0c;我是阿昌&#xff0c;今天学习记录的是关于MySQL的“饮鸩止渴”提高性能的方法的内容。 不知道在实际运维过程中有没有碰到这样的情景&#xff1a; 业务高峰期&#xff0c;生产环境的 MySQL 压力太大&#xff0c;没法正常响…...

08- 数据升维 (PolynomialFeatures) (机器学习)

在做数据升维的时候&#xff0c;最常见的手段就是将已知维度进行相乘&#xff08;或者自乘&#xff09;来构建新的维度 使用 np.concatenate()进行简单的&#xff0c;幂次合并&#xff0c;注意数据合并的方向axis 1 数据可视化时&#xff0c;注意切片&#xff0c;因为数据升维…...

2023备战金三银四,Python自动化软件测试面试宝典合集(二)

马上就又到了程序员们躁动不安&#xff0c;蠢蠢欲动的季节~这不&#xff0c;金三银四已然到了家门口&#xff0c;元宵节一过后台就有不少人问我&#xff1a;现在外边大厂面试都问啥想去大厂又怕面试挂面试应该怎么准备测试开发前景如何面试&#xff0c;一个程序员成长之路永恒绕…...

笔试题-2023-紫光展锐-数字芯片设计【纯净题目版】

回到首页:2023 数字IC设计秋招复盘——数十家公司笔试题、面试实录 推荐内容:数字IC设计学习比较实用的资料推荐 题目背景 笔试时间:2022.08.24应聘岗位:数字芯片设计工程师笔试时长:90min笔试平台:nowcoder牛客网题目类型:单选题(18道)、不定项选择题(22道)题目评…...

WordPress网站日主题Ri主题RiProV2主题开启了验证码登录但是验证码配置不对结果退出登录后进不去管理端了

背景 WordPress网站日主题Ri主题RiProV2主题开启了验证码登录但是验证码配置不对结果退出登录后进不去管理端了;开启了腾讯云验证码防火墙但APPID,APPSecret没配置,结果在退出登录后,由于验证码验证失败管理端进不去了 提示如下:...

自动驾驶感知——毫米波雷达

文章目录1. 雷达的基本概念1.1 毫米波雷达分类1.2 信息的传输1.3 毫米波雷达的信号频段1.4 毫米波雷达工作原理1.4.1 毫米波雷达测速测距的数学原理1.4.2 毫米波雷达测角度的数学原理1.4.3 硬件接口1.4.4 关键零部件1.4.5 数据的协议与格式1.5 车载毫米波雷达的重要参数1.6 车载…...

取电芯片全协议都可兼容

乐得瑞PD协议芯片/PD取电芯片/PD受电端协议芯片 支持5/9/12/15/20v定制 1、概述 LDR6328S 是乐得瑞科技有限公司开发的一款兼容 USB PD、QC 和 AFC 协议的 Sink 控制器。 LDR6328S 从支持 USB PD、QC 和 AFC 协议的适配器取电&#xff0c;然后供电给设备。比如可以配置适配器输…...

自己总结优化代码写法

jdk1.7新特性详解 开发期间略知jdk1.7的一些特性&#xff0c;没有真正的一个一个得展开研究&#xff0c;而是需要说明再去查&#xff0c;导致最整个新特性不是特别的清楚&#xff0c;这种情况以后得需要改变了&#xff0c;否则就会变成代码的奴隶。现在正好有时间可以细细的研…...

Java体系最强干货分享—挑战40天准备Java面试,最快拿到offer!

如何准备java面试&#xff0c;顺利上岸大厂java岗位&#xff1f; 主攻Java的人越来越多&#xff0c;导致行业越来越卷&#xff0c;最开始敲个“hello world”都能进大厂&#xff0c;现在&#xff0c;八股、全家桶、算法等等面试题横行&#xff0c;卷到极致&#xff01;就拿今年…...

基于FPGA的PID算法学习———实现PID比例控制算法

基于FPGA的PID算法学习 前言一、PID算法分析二、PID仿真分析1. PID代码2.PI代码3.P代码4.顶层5.测试文件6.仿真波形 总结 前言 学习内容&#xff1a;参考网站&#xff1a; PID算法控制 PID即&#xff1a;Proportional&#xff08;比例&#xff09;、Integral&#xff08;积分&…...

HTML 列表、表格、表单

1 列表标签 作用&#xff1a;布局内容排列整齐的区域 列表分类&#xff1a;无序列表、有序列表、定义列表。 例如&#xff1a; 1.1 无序列表 标签&#xff1a;ul 嵌套 li&#xff0c;ul是无序列表&#xff0c;li是列表条目。 注意事项&#xff1a; ul 标签里面只能包裹 li…...

dedecms 织梦自定义表单留言增加ajax验证码功能

增加ajax功能模块&#xff0c;用户不点击提交按钮&#xff0c;只要输入框失去焦点&#xff0c;就会提前提示验证码是否正确。 一&#xff0c;模板上增加验证码 <input name"vdcode"id"vdcode" placeholder"请输入验证码" type"text&quo…...

【论文阅读28】-CNN-BiLSTM-Attention-(2024)

本文把滑坡位移序列拆开、筛优质因子&#xff0c;再用 CNN-BiLSTM-Attention 来动态预测每个子序列&#xff0c;最后重构出总位移&#xff0c;预测效果超越传统模型。 文章目录 1 引言2 方法2.1 位移时间序列加性模型2.2 变分模态分解 (VMD) 具体步骤2.3.1 样本熵&#xff08;S…...

【7色560页】职场可视化逻辑图高级数据分析PPT模版

7种色调职场工作汇报PPT&#xff0c;橙蓝、黑红、红蓝、蓝橙灰、浅蓝、浅绿、深蓝七种色调模版 【7色560页】职场可视化逻辑图高级数据分析PPT模版&#xff1a;职场可视化逻辑图分析PPT模版https://pan.quark.cn/s/78aeabbd92d1...

人工智能(大型语言模型 LLMs)对不同学科的影响以及由此产生的新学习方式

今天是关于AI如何在教学中增强学生的学习体验&#xff0c;我把重要信息标红了。人文学科的价值被低估了 ⬇️ 转型与必要性 人工智能正在深刻地改变教育&#xff0c;这并非炒作&#xff0c;而是已经发生的巨大变革。教育机构和教育者不能忽视它&#xff0c;试图简单地禁止学生使…...

[免费]微信小程序问卷调查系统(SpringBoot后端+Vue管理端)【论文+源码+SQL脚本】

大家好&#xff0c;我是java1234_小锋老师&#xff0c;看到一个不错的微信小程序问卷调查系统(SpringBoot后端Vue管理端)【论文源码SQL脚本】&#xff0c;分享下哈。 项目视频演示 【免费】微信小程序问卷调查系统(SpringBoot后端Vue管理端) Java毕业设计_哔哩哔哩_bilibili 项…...

RSS 2025|从说明书学习复杂机器人操作任务:NUS邵林团队提出全新机器人装配技能学习框架Manual2Skill

视觉语言模型&#xff08;Vision-Language Models, VLMs&#xff09;&#xff0c;为真实环境中的机器人操作任务提供了极具潜力的解决方案。 尽管 VLMs 取得了显著进展&#xff0c;机器人仍难以胜任复杂的长时程任务&#xff08;如家具装配&#xff09;&#xff0c;主要受限于人…...

[大语言模型]在个人电脑上部署ollama 并进行管理,最后配置AI程序开发助手.

ollama官网: 下载 https://ollama.com/ 安装 查看可以使用的模型 https://ollama.com/search 例如 https://ollama.com/library/deepseek-r1/tags # deepseek-r1:7bollama pull deepseek-r1:7b改token数量为409622 16384 ollama命令说明 ollama serve #&#xff1a…...

脑机新手指南(七):OpenBCI_GUI:从环境搭建到数据可视化(上)

一、OpenBCI_GUI 项目概述 &#xff08;一&#xff09;项目背景与目标 OpenBCI 是一个开源的脑电信号采集硬件平台&#xff0c;其配套的 OpenBCI_GUI 则是专为该硬件设计的图形化界面工具。对于研究人员、开发者和学生而言&#xff0c;首次接触 OpenBCI 设备时&#xff0c;往…...