Unity3D开发AI桌面精灵/宠物系列 【三】 语音识别 ASR 技术、语音转文本多平台 - 支持科大讯飞、百度等 C# 开发

Unity3D 交互式AI桌面宠物开发系列【三】ASR 语音识别


该系列主要介绍怎么制作AI桌面宠物的流程,我会从项目开始创建初期到最终可以和AI宠物进行交互为止,项目已经开发完成,我会仔细梳理一下流程,分步讲解。 这篇文章主要讲有关于语音识别 ASR 方面的一些方法。


提示:内容纯个人编写,欢迎评论点赞,来指正我。

文章目录

  • Unity3D 交互式AI桌面宠物开发系列【三】ASR 语音识别
  • 前言
  • 一、语音识别 (ASR) 概述
  • 二、Unity开发准备阶段
    • 1.Unity平台
    • 2.示例:讯飞平台
  • 三、科大讯飞 ASR 短语音识别API
    • 1. 创建应用
    • 2. 查看免费测试服务量
    • 3. 查看 WebSocket 接口信息
    • 4. API 开发文档查看
  • 四、功能开发阶段
    • 1. Unity界面开发
    • 2. 代码分析
    • 3. 代码片段
    • 4. 数据配置
    • 5. 效果展示
    • 6. 问题反馈
    • 然后就,大功告成了!!!
  • 总结


前言

本篇内容主要讲Unity开发桌面宠物的语音识别功能,大家感兴趣也可以了解一下这个开发方向,目前还是挺有前景的,AI智能科技发展这么迅猛,紧跟步伐哈~

下面让我们出发吧 ------------>----------------->


一、语音识别 (ASR) 概述

语音识别(Automatic Speech Recognition,ASR)是一种技术,指的是通过计算机程序和算法,将人类所说的语音信号转化为文字或其他形式的电子文本的过程。ASR系统通过分析语音信号中包含的声音波形、频谱和语音特征等信息,识别并转录出语音中所包含的文字内容。这项技术在语音识别软件、智能语音助手、语音搜索、电话客服系统等领域有着广泛的应用。

在这里插入图片描述

二、Unity开发准备阶段

1.Unity平台

  • 该系列全部使用Unity2021.3.44开发;
  • 该系列前后文章存在关联,不懂的可以看前面文章;
  • 该系列完成之后我会上传源码工程,着急的小伙伴可以自己写框架,我就先编写各模块的独立功能。

2.示例:讯飞平台

  • 注册讯飞账户,已注册的直接登录;
  • 创建语音识别应用,然后 领取 语音识别 (短语音识别) 免费测试服务量绑定该应用,或者付费购买服务量;(个人认证可以领取免费服务量)
  • 前两步我就不贴流程图了,该系列在上节的语音唤醒文章中提到过注册和领取相关功能免费服务量的流程;

  • 上述操作很简单
  • 重点来了,科大讯飞平台的ASR接口怎么接入,下面来看一下吧

三、科大讯飞 ASR 短语音识别API

1. 创建应用

在这里插入图片描述在这里插入图片描述

  • 点击用户下面的 “我的应用” 然后创建应用,命名语音识别的应用,创建成功后会生成一个APPKey或者APPID 保存好。(名字随便起,记住APPID,后续要用到)

2. 查看免费测试服务量

在这里插入图片描述

  • 个人测试的话领取免费开发测试服务量就可以了,商用的话就购买相应的服务量。

3. 查看 WebSocket 接口信息

在这里插入图片描述

  • 记录保存该各项数据,后续在WebSocket连接的时候要用到这些。

4. API 开发文档查看

在这里插入图片描述

  • 这里有开发文档可以自行查看并学习,当然我是看过的,那么接下来就到了开发阶段了!
  • ps: 记住这个接口地址,这是WebSocket的API 地址 (调用的网址),接下来会用到。

四、功能开发阶段

1. Unity界面开发

在这里插入图片描述

  • ① 用于显示识别内容的 Text 文本
  • ② 用于按住进行录音操作的 Button 按钮
  • ③ 用于显示按钮的提示词的 Text 文本
  • ④ 用于挂载ASR语音识别 脚本 的空物体

2. 代码分析

在这里插入图片描述

  • 参数:用于定义变量;
  • 语音输入:用于录制音频片段;
  • 获取鉴权Url:用于鉴权加密的API地址;
  • 语音识别:用于实现语音识别的API接口调用功能;
  • 工具方法:用于处理Unity中的音频片段,转换成讯飞标准格式;
  • 数据定义:用于定义发送数据和接收数据的数据结构,根据第三方平台的参数数据。

3. 代码片段

using System;
using System.Collections;
using System.Collections.Generic;
using System.Net.WebSockets;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;public class XunfeiSpeechToText : MonoBehaviour
{#region 参数/// <summary>/// host地址/// </summary>[SerializeField] private string m_HostUrl = "iat-api.xfyun.cn";/// <summary>/// 语言/// </summary>[SerializeField] private string m_Language = "zh_cn";/// <summary>/// 应用领域/// </summary>[SerializeField] private string m_Domain = "iat";/// <summary>/// 方言mandarin:中文普通话、其他语种/// </summary>[SerializeField] private string m_Accent = "mandarin";/// <summary>/// 音频的采样率/// </summary>[SerializeField] private string m_Format = "audio/L16;rate=16000";/// <summary>/// 音频数据格式/// </summary>[SerializeField] private string m_Encoding = "raw";/// <summary>/// websocket/// </summary>private ClientWebSocket m_WebSocket;/// <summary>/// 传输中断标记点/// </summary>private CancellationToken m_CancellationToken;/// <summary>/// 语音识别API地址/// </summary>[SerializeField][Header("语音识别API地址")]private string m_SpeechRecognizeURL;/// <summary>/// 讯飞的AppID/// </summary>[Header("填写APP ID")][SerializeField] private string m_AppID = "讯飞的AppID";/// <summary>/// 讯飞的APIKey/// </summary>[Header("填写Api Key")][SerializeField] private string m_APIKey = "讯飞的APIKey";/// <summary>/// 讯飞的APISecret/// </summary>[Header("填写Secret Key")][SerializeField] private string m_APISecret = "讯飞的APISecret";/// <summary>/// 计算方法调用的时间/// </summary>[SerializeField] protected System.Diagnostics.Stopwatch stopwatch = new System.Diagnostics.Stopwatch();#endregionprivate void Awake(){//注册按钮事件RegistButtonEvent();//绑定地址 地址就是讯飞平台上的 WebSocket API地址m_SpeechRecognizeURL = "wss://iat-api.xfyun.cn/v2/iat";}#region 语音输入/// <summary>/// 语音输入的按钮/// </summary>[SerializeField] private Button m_VoiceInputBotton;/// <summary>/// 录音按钮的文本/// </summary>[SerializeField] private Text m_VoiceBottonText;/// <summary>/// 录音的提示信息/// </summary>[SerializeField] private Text m_RecordTips;/// <summary>/// 录制的音频长度/// </summary>public int m_RecordingLength = 5;/// <summary>/// 临时接收音频的片段/// </summary>private AudioClip recording;/// <summary>/// 注册按钮事件/// </summary>private void RegistButtonEvent(){if (m_VoiceInputBotton == null || m_VoiceInputBotton.GetComponent<EventTrigger>())return;EventTrigger _trigger = m_VoiceInputBotton.gameObject.AddComponent<EventTrigger>();//添加按钮按下的事件EventTrigger.Entry _pointDown_entry = new EventTrigger.Entry();_pointDown_entry.eventID = EventTriggerType.PointerDown;_pointDown_entry.callback = new EventTrigger.TriggerEvent();//添加按钮松开事件EventTrigger.Entry _pointUp_entry = new EventTrigger.Entry();_pointUp_entry.eventID = EventTriggerType.PointerUp;_pointUp_entry.callback = new EventTrigger.TriggerEvent();//添加委托事件_pointDown_entry.callback.AddListener(delegate { StartRecord(); });_pointUp_entry.callback.AddListener(delegate { StopRecord(); });_trigger.triggers.Add(_pointDown_entry);_trigger.triggers.Add(_pointUp_entry);}/// <summary>/// 开始录制/// </summary>public void StartRecord(){m_VoiceBottonText.text = "正在录音中...";StartRecordAudio();}/// <summary>/// 结束录制/// </summary>public void StopRecord(){m_VoiceBottonText.text = "按住按钮,开始录音";m_RecordTips.text = "录音结束,正在识别...";StopRecordAudio(AcceptClip);}/// <summary>/// 开始录制声音/// </summary>public void StartRecordAudio(){recording = Microphone.Start(null, true, m_RecordingLength, 16000);}/// <summary>/// 结束录制,返回audioClip/// </summary>/// <param name="_callback"></param>public void StopRecordAudio(Action<AudioClip> _callback){Microphone.End(null);_callback(recording);}/// <summary>/// 处理录制的音频数据/// </summary>/// <param name="_data"></param>public void AcceptClip(AudioClip _audioClip){m_RecordTips.text = "正在进行语音识别...";SpeechToText(_audioClip, DealingTextCallback);}/// <summary>/// 处理识别到的文本/// </summary>/// <param name="_msg"></param>private void DealingTextCallback(string _msg){//在此处处理接收到的数据,可以选择发送给大模型,或者打印测试,会在后续补充功能m_RecordTips.text = _msg;Debug.Log(_msg);}#endregion#region 获取鉴权Url/// <summary>/// 获取鉴权url/// </summary>/// <returns></returns>private string GetUrl(){//获取时间戳string date = DateTime.Now.ToString("r");//拼接原始的signaturestring signature_origin = string.Format("host: " + m_HostUrl + "\ndate: " + date + "\nGET /v2/iat HTTP/1.1");//hmac-sha256算法-签名,并转换为base64编码string signature = Convert.ToBase64String(new HMACSHA256(Encoding.UTF8.GetBytes(m_APISecret)).ComputeHash(Encoding.UTF8.GetBytes(signature_origin)));//拼接原始的authorizationstring authorization_origin = string.Format("api_key=\"{0}\",algorithm=\"hmac-sha256\",headers=\"host date request-line\",signature=\"{1}\"", m_APIKey, signature);//转换为base64编码string authorization = Convert.ToBase64String(Encoding.UTF8.GetBytes(authorization_origin));//拼接鉴权的urlstring url = string.Format("{0}?authorization={1}&date={2}&host={3}", m_SpeechRecognizeURL, authorization, date, m_HostUrl);return url;}#endregion#region 语音识别/// <summary>/// 语音识别/// </summary>/// <param name="_clip"></param>/// <param name="_callback"></param>public void SpeechToText(AudioClip _clip, Action<string> _callback){byte[] _audioData = ConvertClipToBytes(_clip);StartCoroutine(SendAudioData(_audioData, _callback));}/// <summary>/// 识别短文本/// </summary>/// <param name="_audioData"></param>/// <param name="_callback"></param>/// <returns></returns>public IEnumerator SendAudioData(byte[] _audioData, Action<string> _callback){yield return null;ConnetHostAndRecognize(_audioData, _callback);}/// <summary>/// 连接服务,开始识别/// </summary>/// <param name="_audioData"></param>/// <param name="_callback"></param>private async void ConnetHostAndRecognize(byte[] _audioData, Action<string> _callback){try{stopwatch.Restart();//建立socket连接m_WebSocket = new ClientWebSocket();m_CancellationToken = new CancellationToken();Uri uri = new Uri(GetUrl());await m_WebSocket.ConnectAsync(uri, m_CancellationToken);//开始识别SendVoiceData(_audioData, m_WebSocket);StringBuilder stringBuilder = new StringBuilder();while (m_WebSocket.State == WebSocketState.Open){var result = new byte[4096];await m_WebSocket.ReceiveAsync(new ArraySegment<byte>(result), m_CancellationToken);//去除空字节List<byte> list = new List<byte>(result); while (list[list.Count - 1] == 0x00) list.RemoveAt(list.Count - 1);string str = Encoding.UTF8.GetString(list.ToArray());//获取返回的jsonResponseData _responseData = JsonUtility.FromJson<ResponseData>(str);if (_responseData.code == 0){stringBuilder.Append(GetWords(_responseData));}else{PrintErrorLog(_responseData.code);}m_WebSocket.Abort();}string _resultMsg = stringBuilder.ToString();//识别成功,回调_callback(_resultMsg);stopwatch.Stop();if (_resultMsg.Equals(null) || _resultMsg.Equals("")){Debug.Log("语音识别为空字符串");}else{//识别的数据不为空 在此处做功能处理}Debug.Log("讯飞语音识别耗时:" + stopwatch.Elapsed.TotalSeconds);}catch (Exception ex){Debug.LogError("报错信息: " + ex.Message);m_WebSocket.Dispose();}}/// <summary>/// 获取识别到的文本/// </summary>/// <param name="_responseData"></param>/// <returns></returns>private string GetWords(ResponseData _responseData){StringBuilder stringBuilder = new StringBuilder();foreach (var item in _responseData.data.result.ws){foreach (var _cw in item.cw){stringBuilder.Append(_cw.w);}}return stringBuilder.ToString();}private void SendVoiceData(byte[] audio, ClientWebSocket socket){if (socket.State != WebSocketState.Open){return;}PostData _postData = new PostData(){common = new CommonTag(m_AppID),business = new BusinessTag(m_Language, m_Domain, m_Accent),data = new DataTag(2, m_Format, m_Encoding, Convert.ToBase64String(audio))};string _jsonData = JsonUtility.ToJson(_postData);//发送数据socket.SendAsync(new ArraySegment<byte>(Encoding.UTF8.GetBytes(_jsonData)), WebSocketMessageType.Binary, true, new CancellationToken());}#endregion#region 工具方法/// <summary>/// audioclip转为byte[]/// </summary>/// <param name="audioClip"></param>/// <returns></returns>public byte[] ConvertClipToBytes(AudioClip audioClip){float[] samples = new float[audioClip.samples];audioClip.GetData(samples, 0);short[] intData = new short[samples.Length];byte[] bytesData = new byte[samples.Length * 2];int rescaleFactor = 32767;for (int i = 0; i < samples.Length; i++){intData[i] = (short)(samples[i] * rescaleFactor);byte[] byteArr = new byte[2];byteArr = BitConverter.GetBytes(intData[i]);byteArr.CopyTo(bytesData, i * 2);}return bytesData;}/// <summary>/// 打印错误日志/// </summary>/// <param name="status"></param>private void PrintErrorLog(int status){if (status == 10005){Debug.LogError("appid授权失败");return;}if (status == 10006){Debug.LogError("请求缺失必要参数");return;}if (status == 10007){Debug.LogError("请求的参数值无效");return;}if (status == 10010){Debug.LogError("引擎授权不足");return;}if (status == 10019){Debug.LogError("session超时");return;}if (status == 10043){Debug.LogError("音频解码失败");return;}if (status == 10101){Debug.LogError("引擎会话已结束");return;}if (status == 10313){Debug.LogError("appid不能为空");return;}if (status == 10317){Debug.LogError("版本非法");return;}if (status == 11200){Debug.LogError("没有权限");return;}if (status == 11201){Debug.LogError("日流控超限");return;}if (status == 10160){Debug.LogError("请求数据格式非法");return;}if (status == 10161){Debug.LogError("base64解码失败");return;}if (status == 10163){Debug.LogError("缺少必传参数,或者参数不合法,具体原因见详细的描述");return;}if (status == 10200){Debug.LogError("读取数据超时");return;}if (status == 10222){Debug.LogError("网络异常");return;}}#endregion#region 数据定义/// <summary>/// 发送的数据/// </summary>[Serializable]public class PostData{[SerializeField] public CommonTag common;[SerializeField] public BusinessTag business;[SerializeField] public DataTag data;}[Serializable]public class CommonTag{[SerializeField] public string app_id = string.Empty;public CommonTag(string app_id){this.app_id = app_id;}}[Serializable]public class BusinessTag{[SerializeField] public string language = "zh_cn";[SerializeField] public string domain = "iat";[SerializeField] public string accent = "mandarin";public BusinessTag(string language, string domain, string accent){this.language = language;this.domain = domain;this.accent = accent;}}[Serializable]public class DataTag{[SerializeField] public int status = 2;[SerializeField] public string format = "audio/L16;rate=16000";[SerializeField] public string encoding = "raw";[SerializeField] public string audio = string.Empty;public DataTag(int status, string format, string encoding, string audio){this.status = status;this.format = format;this.encoding = encoding;this.audio = audio;}}[Serializable]public class ResponseData{[SerializeField] public int code = 0;[SerializeField] public string message = string.Empty;[SerializeField] public string sid = string.Empty;[SerializeField] public ResponcsedataTag data;}[Serializable]public class ResponcsedataTag{[SerializeField] public Results result;[SerializeField] public int status = 2;}[Serializable]public class Results{[SerializeField] public List<WsTag> ws;}[Serializable]public class WsTag{[SerializeField] public List<CwTag> cw;}[Serializable]public class CwTag{[SerializeField] public int sc = 0;[SerializeField] public string w = string.Empty;}#endregion}

4. 数据配置

在这里插入图片描述

  • 将APPid、ApiKey、SecretKey替换成自己讯飞平台上的 WebSocket 信息数据
  • 变量对应UI进行配置

5. 效果展示

在这里插入图片描述

  • 按住按钮进行录音,松开后进行识别,返回的结果会显示在UI界面上,就代表成功了。

在这里插入图片描述

  • 控制台会有打印结果的,也一样代表 API 接入成功。
  • 注意使用时,打开电脑麦克风权限

6. 问题反馈

  • 运行后有任何问题都可以在评论区进行讨论~
  • 代码写的不是很工整,多多指点,后续会进行整个系列的框架搭建
  • 下一期更新暂定:
    ① 其他平台ASR接入功能,例如:百度等
    ② 大模型LLM的接入,例如:讯飞、百度、GPT等
    二选一哦!
  • 评论告诉我,下一期更新什么

然后就,大功告成了!!!

在这里插入图片描述
比心啦 ❥(^_-)

在这里插入图片描述

总结

  • 提示: 大家根据需求来做功能,后续继续其他功能啦,不懂的快喊我。
  • 大家可以在评论区讨论其他系列下一期出什么内容,这个系列会继续更新的
  • 点赞收藏加关注哦~ 蟹蟹

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/76617.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Java 状态模式 详解

状态模式详解 一、状态模式概述 状态模式(State Pattern)是一种行为型设计模式&#xff0c;它允许一个对象在其内部状态改变时改变它的行为&#xff0c;使对象看起来似乎修改了它的类。 核心特点 状态封装&#xff1a;将每个状态的行为封装到独立的类中状态转换&#xff1a…

Nginx 配置 HTTPS 与 WSS 完整指南

Nginx 配置 HTTPS 与 WSS 完整指南 本教程将手把手教你如何为网站配置 HTTPS 加密访问&#xff0c;并通过反向代理实现安全的 WebSocket&#xff08;WSS&#xff09;通信。以 https://www.zhegepai.cn 域名为例&#xff0c;完整流程约需 30 分钟完成。 一、前置准备 1.1 域名…

双向链表的理解

背景 代码中经常会出现双向链表&#xff0c;对于双向链表的插入和删除有对应的API函数接口&#xff0c;但直观的图表更容易理解&#xff0c;所以本文会对rt-thread内核代码中提供的双向链表的一些API函数操作进行绘图&#xff0c;方便后续随时查看。 代码块 rt-thread中提供…

大文件上传源码,支持单个大文件与多个大文件

大文件上传源码&#xff0c;支持单个大文件与多个大文件 Ⅰ 思路Ⅱ 具体代码前端--单个大文件前端--多个大文件前端接口后端 Ⅰ 思路 具体思路请参考我之前的文章&#xff0c;这里分享的是上传流程与源码 https://blog.csdn.net/sugerfle/article/details/130829022 Ⅱ 具体代码…

Unity中的静态合批使用整理

静态批处理是一种绘制调用批处理方法&#xff0c;它组合不移动的网格以减少绘制调用。它将组合的网格转换为世界空间&#xff0c;并为它们构建一个共享顶点和索引缓冲区。然后&#xff0c;对于可见网格&#xff0c;Unity 会执行一系列简单的绘制调用&#xff0c;每个调用之间几…

【机器学习中的基本术语:特征、样本、训练集、测试集、监督/无监督学习】

机器学习基本术语详解 1. 特征&#xff08;Feature&#xff09; 定义&#xff1a;数据的属性或变量&#xff0c;用于描述样本的某个方面。作用&#xff1a;模型通过学习特征与目标之间的关系进行预测。示例&#xff1a; 预测房价时&#xff0c;特征可以是 面积、地段、房龄。…

C++学习之路:指针基础

目录 指针介绍与基本用法双重指针函数指针空指针与野指针函数参数的指针传递最后 指针一般在C/C语言学习的后期接触&#xff0c;这样就导致指针给新手一种高深莫测、难以掌握的刻板印象。但实际上指针的使用很简单&#xff0c;并且还能够极大的提高程序的灵活性&#xff0c;帮助…

【服务日志链路追踪】

MDCInheritableThreadLocal和spring cloud sleuth 在微服务架构中&#xff0c;日志链路追踪&#xff08;Logback Distributed Tracing&#xff09; 是一个关键需求&#xff0c;主要用于跟踪请求在不同服务间的调用链路&#xff0c;便于排查问题。常见的实现方案有两种&#x…

Kafka+Zookeeper从docker部署到spring boot使用完整教程

文章目录 一、Kafka1.Kafka核心介绍&#xff1a;​核心架构​核心特性​典型应用 2.Kafka对 ZooKeeper 的依赖&#xff1a;3.去 ZooKeeper 的演进之路&#xff1a;注&#xff1a;&#xff08;本文采用ZooKeeper3.8 Kafka2.8.1&#xff09; 二、Zookeeper1.核心架构与特性2.典型…

JUC系列JMM学习之随笔

JUC: JUC 是 Java 并发编程的核心工具包,全称为 Java Util Concurrent,是 java.util.concurrent 包及其子包的简称。它提供了一套强大且高效的并发编程工具,用于简化多线程开发并提高性能。 CPU核心数和线程数的关系:1核处理1线程(同一时间单次) CPU内核结构: 工作内…

The Rust Programming Language 学习 (九)

泛型 每一个编程语言都有高效处理重复概念的工具。在 Rust 中其工具之一就是 泛型&#xff08;generics&#xff09;。泛型是具体类型或其他属性的抽象替代。我们可以表达泛型的属性&#xff0c;比如他们的行为或如何与其他泛型相关联&#xff0c;而不需要在编写和编译代码时知…

蓝桥杯 混乘数字

问题描述 混乘数字的定义如下&#xff1a; 对于一个正整数 n&#xff0c;如果存在正整数 a 和 b&#xff0c;使得&#xff1a; n a b且 a 与 b 的十进制数位中每个数字出现的次数之和&#xff0c;与 n 中对应数字出现的次数相同&#xff0c;则称 n 为混乘数字。 示例 对于…

CExercise04_1位运算符_2 定义一个函数判断给定的正整数是否为2的幂

题目&#xff1a; 给定一个正整数&#xff0c;请定义一个函数判断它是否为2的幂(1, 2, 4, 8, 16, …) 分析&#xff1a; &#xff1a; 代码 #define _CRT_SECURE_NO_WARNINGS #include <stdio.h> #include <stdbool.h>/* 给定一个正整数&#xff0c;请定义一个函数…

SSL证书不可信的原因有哪些?(国科云)

SSL证书用于在客户端和服务器端之间建立一条加密通道&#xff0c;确保数据在传输过程中的安全性和完整性。然而&#xff0c;在实际应用中&#xff0c;我们有时会遇到SSL证书不可信的情况&#xff0c;严重影响了用户对网站的信任度。那么&#xff0c;SSL证书不可信的原因究竟有哪…

[王阳明代数讲义]琴语言类型系统工程特性

琴语言类型系统工程特性 层展物理学组织实务与艺术与琴生生.物机.械科.技工.业研究.所软凝聚态物理开发工具包社会科学气质砥砺学人生意气场社群成员魅力场与心气微积分社会关系力学 意气实体过程图论信息编码&#xff0c;如来码导引 注意力机制道装Transformer架构的发展标度律…

自抗扰ADRC之二阶线性扩展状态观测器(LESO)推导

1.龙伯格观测器 实际工程应用中&#xff0c;状态变量有时难以使用传感器直接测量&#xff0c;在这种情况下&#xff0c;使用状态观测器估计系统实际状态是非常常见的做法。最出名的状态观测器当属龙伯格博士在1971年发表于TAC的An Introduction to Observer[1]一文中提出的基于…

从头开发一个Flutter插件(二)高德地图定位插件

开发基于高德定位SDK的Flutter插件 在上一篇文章里具体介绍了Flutter插件的具体开发流程&#xff0c;从创建项目到发布。接下来将为Flutter天气项目开发一个基于高德定位SDK的Flutter定位插件。 申请key 首先进入高德地图定位SDK文档内下载定位SDK&#xff0c;并按要求申请A…

分布式锁之redis6

一、分布式锁介绍 之前我们都是使用本地锁&#xff08;synchronize、lock等&#xff09;来避免共享资源并发操作导致数据问题&#xff0c;这种是锁在当前进程内。 那么在集群部署下&#xff0c;对于多个节点&#xff0c;我们要使用分布式锁来避免共享资源并发操作导致数据问题…

ubuntu中使用安卓模拟器

本文这里介绍 使用 android studio Emulator &#xff0c; 当然也有 Anbox (Lightweight)&#xff0c; Waydroid (Best for Full Android Experience), 首先确保自己安装了 android studio &#xff1b; sudo apt update sudo apt install openjdk-11-jdk sudo snap install…

二语习得理论(Second Language Acquisition, SLA)如何学习英语

二语习得理论&#xff08;Second Language Acquisition, SLA&#xff09;是研究学习者如何在成人或青少年阶段学习第二语言&#xff08;L2&#xff09;的理论框架。该理论主要关注语言习得过程中的认知、社会和文化因素&#xff0c;解释了学习者如何从初学者逐渐变得流利并能够…