OpenSL ES全称为Open Sound Library for Embedded Systems。OpenSL ES是无授权费、跨平台、针对嵌入式系统 精心优化的硬件音频加速API。当然安卓也使用了它,AudioTrack、MediaPlayer的音频播放,底层都是利用OpenSL。和AudioTrack相同,它只接受流,不支持音频数据的编解码,所以需要结合第三方库来使用。

为什么要使用OpenSL?一般应用使用安卓SDK提供的API就足够满足使用,但对于专门音频播放的APP,系统提供的方法就有点力不从心了,比如:AudioTrack利用native层调用OpenSL,必不可免的要进行java层流数据转化为native层流数据,这就会造成一定延迟,而对于音频播放APP而言,这是不可接受的。
前面我们使用了FFmpeg解码音频流,并使用了java层的AudioTrack进行播放,今天来实现在native层直接使用OpenSL播放。
由于OpenSL系统底层本身就集成,我们只需要在CMakeLists中导入系统动态库就可以了。
target_link_libraries(native-libavcodec-56avdevice-56avfilter-5avformat-56avutil-54postproc-53swresample-1swscale-3android//openSL库OpenSLES${log-lib})
Opensl的套路如下:

1、创建引擎接口对象
2、创建混音器
3、创建播放器(录音器)
4、设置缓冲队列和回调函数
5、设置播放状态
6、启动回调函数

这边现在java中编写对应native中的方法
package com.aruba.ffmpegapplication;import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioTrack;
import android.os.Bundle;
import android.os.Environment;
import android.view.View;import androidx.appcompat.app.AppCompatActivity;import java.io.File;public class PcmPlayActivity extends AppCompatActivity {static {System.loadLibrary("native-lib");}private AudioTrack audioTrack;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_pcm_play);}/*** 给native层回调** @param sampleRateInHz* @param channelCount*/private void create(int sampleRateInHz, int channelCount) {int channelConfig = AudioFormat.CHANNEL_OUT_MONO; //单声道if (channelCount == 2) {channelConfig = AudioFormat.CHANNEL_OUT_STEREO;}int buffSize = AudioTrack.getMinBufferSize(sampleRateInHz, channelConfig, AudioFormat.ENCODING_PCM_16BIT);//计算最小缓冲区//    @Deprecated
//    public AudioTrack(int streamType, int sampleRateInHz, int channelConfig, int audioFormat, int bufferSizeInBytes, int mode) throws IllegalArgumentException {
//            throw new RuntimeException("Stub!");
//        }audioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, sampleRateInHz, channelConfig, AudioFormat.ENCODING_PCM_16BIT, buffSize, AudioTrack.MODE_STREAM);audioTrack.play();}/*** 给native层回调*/private void play(byte[] bytes, int size) {if (audioTrack != null && audioTrack.getPlayState() == AudioTrack.PLAYSTATE_PLAYING)audioTrack.write(bytes, 0, size);}public void click(View view) {switch (view.getId()) {case R.id.btn_audiotrack:final File input1 = new File(Environment.getExternalStorageDirectory(), "input.mp3");new Thread() {@Overridepublic void run() {playByAudio(input1.getAbsolutePath());}}.start();break;case R.id.btn_opensl:final File input2 = new File(Environment.getExternalStorageDirectory(), "input.mp3");new Thread() {@Overridepublic void run() {playByOpenSL(input2.getAbsolutePath());}}.start();break;case R.id.btn_opensl_stop:stopByOpenSL();break;}}private native void playByAudio(String inputFilePath);private native void playByOpenSL(String inputFilePath);private native void stopByOpenSL();
}
在native层将opensl封装成一个类

_opensl_helper.h

//
// Created by aruba on 2020/7/6.
//#ifndef FFMPEGAPPLICATION_OPENSL_HELPER_H
#define FFMPEGAPPLICATION_OPENSL_HELPER_H#include <SLES/OpenSLES.h>
#include <SLES/OpenSLES_Android.h>class OpenslHelper {
public://返回结果SLresult result;//opensl引擎SLObjectItf engine;//引擎接口SLEngineItf engineInterface;//混音器SLObjectItf mix;//环境混响混音器接口SLEnvironmentalReverbItf environmentalReverbItf;//环境混响混音器环境const SLEnvironmentalReverbSettings settings = SL_I3DL2_ENVIRONMENT_PRESET_DEFAULT;//播放器SLObjectItf player;//播放接口SLPlayItf playInterface;//缓冲区SLAndroidSimpleBufferQueueItf bufferQueueItf;//音量SLVolumeItf volumeItf;//播放状态 SL_PLAYSTATE_XXXSLuint32 playState;/*** 创建opensl引擎接口* @return SLresult*/SLresult createEngine();/*** 是否成功* @param result * @return */bool isSuccess(SLresult &result);/*** 创建混音器* @return SLresult*/SLresult createMix();/*** 创建播放器* @param numChannels   声道数* @param samplesRate   采样率 SL_SAMPLINGRATE_XX* @param bitsPerSample 采样位数(量化格式) SL_PCMSAMPLEFORMAT_FIXED_XX* @param channelMask   立体声掩码 SL_SPEAKER_XX | SL_SPEAKER_XX* @return SLresult*/SLresult createPlayer(int numChannels, long samplesRate, int bitsPerSample, int channelMask);//播放SLresult play();//暂停SLresult pause();//停止SLresult stop();/*** 播放器会不断调用此函数,我们需要在此回调中不断给缓冲区填充数据* @param bufferQueueItf * @param pContext */SLresultregisterCallback(slAndroidSimpleBufferQueueCallback callback);~OpenslHelper();
};#endif //FFMPEGAPPLICATION_OPENSL_HELPER_H

_opensl_helper.cpp

//
// Created by aruba on 2020/7/6.
//#include "_opensl_helper.h"/*** 是否成功* @param result * @return */
bool OpenslHelper::isSuccess(SLresult &result) {if (result == SL_RESULT_SUCCESS) {return true;}return false;
}/*** 创建opensl引擎接口* @return SLresult*/
SLresult OpenslHelper::createEngine() {//创建引擎result = slCreateEngine(&engine, 0, NULL, 0, NULL, NULL);if (!isSuccess(result)) {return result;}//实例化引擎,第二个参数为:是否异步result = (*engine)->Realize(engine, SL_BOOLEAN_FALSE);if (!isSuccess(result)) {return result;}//获取引擎接口result = (*engine)->GetInterface(engine, SL_IID_ENGINE, &engineInterface);if (!isSuccess(result)) {return result;}return result;
}/*** 创建混音器* @return SLresult*/
SLresult OpenslHelper::createMix() {//获取混音器result = (*engineInterface)->CreateOutputMix(engineInterface, &mix, 0,0, 0);if (!isSuccess(result)) {return result;}//实例化混音器result = (*mix)->Realize(mix, SL_BOOLEAN_FALSE);if (!isSuccess(result)) {return result;}//获取环境混响混音器接口SLresult environmentalResult = (*mix)->GetInterface(mix, SL_IID_ENVIRONMENTALREVERB,&environmentalReverbItf);if (isSuccess(environmentalResult)) {//给混音器设置环境(*environmentalReverbItf)->SetEnvironmentalReverbProperties(environmentalReverbItf,&settings);}return result;
}/*** 创建播放器* @param numChannels   声道数* @param samplesRate   采样率 SL_SAMPLINGRATE_XX* @param bitsPerSample 采样位数(量化格式) SL_PCMSAMPLEFORMAT_FIXED_XX* @param channelMask   立体声掩码 SL_SPEAKER_XX | SL_SPEAKER_XX* @return SLresult*/
SLresult OpenslHelper::createPlayer(int numChannels, long samplesRate, int bitsPerSample,int channelMask) {//1.关联音频流缓冲区  设为2是防止延迟 可以在播放另一个缓冲区时填充新数据SLDataLocator_AndroidSimpleBufferQueue buffQueque = {SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE,2};//缓冲区格式
//    typedef struct SLDataFormat_PCM_ {
//        SLuint32      formatType;    //格式pcm
//        SLuint32      numChannels;   //声道数
//        SLuint32      samplesPerSec; //采样率
//        SLuint32      bitsPerSample; //采样位数(量化格式)
//        SLuint32      containerSize; //包含位数
//        SLuint32      channelMask;   //立体声
//        SLuint32      endianness;    //结束标志位
//    } SLDataFormat_PCM;SLDataFormat_PCM dataFormat_pcm = {SL_DATAFORMAT_PCM, numChannels, samplesRate, bitsPerSample,bitsPerSample, channelMask,SL_BYTEORDER_LITTLEENDIAN};//存放缓冲区地址和格式地址的结构体
//    typedef struct SLDataSource_ {
//        void *pLocator;//缓冲区
//        void *pFormat;//格式
//    } SLDataSource;SLDataSource audioSrc = {&buffQueque, &dataFormat_pcm};//2.关联混音器
//    typedef struct SLDataLocator_OutputMix {
//        SLuint32      locatorType;
//        SLObjectItf       outputMix;
//    } SLDataLocator_OutputMix;SLDataLocator_OutputMix dataLocator_outputMix = {SL_DATALOCATOR_OUTPUTMIX, mix};//混音器快捷方式
//    typedef struct SLDataSink_ {
//        void *pLocator;
//        void *pFormat;
//    } SLDataSink;SLDataSink audioSnk = {&dataLocator_outputMix, NULL};//3.通过引擎接口创建播放器
//    SLresult (*CreateAudioPlayer) (
//            SLEngineItf self,
//            SLObjectItf * pPlayer,
//            SLDataSource *pAudioSrc,
//            SLDataSink *pAudioSnk,
//            SLuint32 numInterfaces,
//            const SLInterfaceID * pInterfaceIds,
//            const SLboolean * pInterfaceRequired
//    );SLInterfaceID ids[3] = {SL_IID_BUFFERQUEUE, SL_IID_EFFECTSEND, SL_IID_VOLUME};SLboolean required[3] = {SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE};result = (*engineInterface)->CreateAudioPlayer(engineInterface, &player, &audioSrc, &audioSnk,3, ids,required);if (!isSuccess(result)) {return result;}//播放器实例化result = (*player)->Realize(player, SL_BOOLEAN_FALSE);if (!isSuccess(result)) {return result;}//获取播放接口result = (*player)->GetInterface(player, SL_IID_PLAY, &playInterface);if (!isSuccess(result)) {return result;}result = (*player)->GetInterface(player, SL_IID_VOLUME, &volumeItf);if (!isSuccess(result)) {return result;}//注册缓冲区result = (*player)->GetInterface(player, SL_IID_BUFFERQUEUE, &bufferQueueItf);if (!isSuccess(result)) {return result;}return result;
}/*** 设置回调接口* @return SLresult*/
SLresult OpenslHelper::registerCallback(slAndroidSimpleBufferQueueCallback callback) {//设置回调接口result = (*bufferQueueItf)->RegisterCallback(bufferQueueItf, callback, NULL);return result;
}//播放
SLresult OpenslHelper::play() {playState = SL_PLAYSTATE_PLAYING;result = (*playInterface)->SetPlayState(playInterface, SL_PLAYSTATE_PLAYING);return result;
}//暂停
SLresult OpenslHelper::pause() {playState = SL_PLAYSTATE_PAUSED;result = (*playInterface)->SetPlayState(playInterface, SL_PLAYSTATE_PAUSED);return result;
}//停止
SLresult OpenslHelper::stop() {playState = SL_PLAYSTATE_STOPPED;result = (*playInterface)->SetPlayState(playInterface, SL_PLAYSTATE_STOPPED);return result;
}//析构
OpenslHelper::~OpenslHelper() {//播放器if (player != NULL) {(*player)->Destroy(player);player = NULL;//播放接口playInterface = NULL;//缓冲区bufferQueueItf = NULL;//音量volumeItf = NULL;}//混音器if (mix != NULL) {(*mix)->Destroy(mix);mix = NULL;//环境混响混音器接口environmentalReverbItf = NULL;}//opensl引擎if (engine != NULL) {(*engine)->Destroy(engine);engine = NULL;//引擎接口engineInterface = NULL;}result = NULL;
}
运用我们之前的ffmpeg代码,调用opensl进行播放
#include <jni.h>
#include <string>
#include <android/log.h>
#include <android/native_window_jni.h>
#include <unistd.h>
#include <pthread.h>
#include <stdio.h>extern "C" {
//编码
#include "libavcodec/avcodec.h"
//封装格式处理
#include "libavformat/avformat.h"
#include "libswresample/swresample.h"
//像素处理
#include "libswscale/swscale.h"
#include "_opensl_helper.h"
}OpenslHelper helper;
uint8_t *out;
int buff_size;
//char *filePath;
AVFormatContext *formatContext;
AVCodecContext *codecContext;
int audio_stream_idx;
AVPacket *pkt;
AVFrame *picture;
SwrContext *swrContext;
int channel_count;
int out_size;void playerCallback(SLAndroidSimpleBufferQueueItf bq, void *pContext);/*** 音频解码PCM,OpenSL播放*/
extern "C"
JNIEXPORT void JNICALL
Java_com_aruba_ffmpegapplication_PcmPlayActivity_playByOpenSL(JNIEnv *env, jobject instance,jstring inputFilePath_) {//初始化openslSLresult result = helper.createEngine();if (!helper.isSuccess(result)) {LOGE("createEngine失败");return;}result = helper.createMix();if (!helper.isSuccess(result)) {LOGE("createMix失败");return;}const char *inputFilePath = env->GetStringUTFChars(inputFilePath_, 0);
//    const int size = sizeof(inputFilePath);
//    filePath = static_cast<char *>(malloc(size));
//    memcpy(filePath, inputFilePath, size);//注册FFmpeg中各大组件av_register_all();//打开文件formatContext = avformat_alloc_context();if (avformat_open_input(&formatContext, inputFilePath, NULL, NULL) != 0) {LOGE("打开失败");avformat_free_context(formatContext);env->ReleaseStringUTFChars(inputFilePath_, inputFilePath);return;}//将文件信息填充进AVFormatContextif (avformat_find_stream_info(formatContext, NULL) < 0) {LOGE("获取文件信息失败");avformat_free_context(formatContext);env->ReleaseStringUTFChars(inputFilePath_, inputFilePath);return;}//获取视频流的编解码器上下文codecContext = NULL;audio_stream_idx = -1;for (int i = 0; i < formatContext->nb_streams; ++i) {if (formatContext->streams[i]->codec->codec_type == AVMEDIA_TYPE_AUDIO) {//如果是音频流codecContext = formatContext->streams[i]->codec;audio_stream_idx = i;break;}}if (codecContext == NULL) {avformat_free_context(formatContext);env->ReleaseStringUTFChars(inputFilePath_, inputFilePath);return;}//根据编解码器上下文的id获取视频流解码器AVCodec *codec = avcodec_find_decoder(codecContext->codec_id);//打开解码器if (avcodec_open2(codecContext, codec, NULL) < 0) {LOGE("解码失败");avformat_free_context(formatContext);env->ReleaseStringUTFChars(inputFilePath_, inputFilePath);return;}//开始读每一帧//存放压缩数据pkt = (AVPacket *) (av_malloc(sizeof(AVPacket)));av_init_packet(pkt);//存放解压数据picture = av_frame_alloc();//音频转码组件上下文swrContext = swr_alloc();//AV_CH_LAYOUT_STEREO:双声道  AV_SAMPLE_FMT_S16:量化格式 16位 codecContext->sample_rate:采样率 Hzswr_alloc_set_opts(swrContext, AV_CH_LAYOUT_STEREO, AV_SAMPLE_FMT_S16,codecContext->sample_rate,//输出采样率和输入采样率应相同codecContext->channel_layout, codecContext->sample_fmt,codecContext->sample_rate, 0, NULL);swr_init(swrContext);//原音频通道数channel_count = av_get_channel_layout_nb_channels(codecContext->channel_layout);//单通道最大存放转码数据 所占字节 = 采样率*量化格式 / 8out_size = 44100 * 16 / 8;out = (uint8_t *) (av_malloc(out_size));//开始播放result = helper.createPlayer(2, SL_SAMPLINGRATE_44_1, SL_PCMSAMPLEFORMAT_FIXED_16,SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT);if (!helper.isSuccess(result)) {LOGE("createPlayer失败");//释放资源av_free(out);out = NULL;swr_free(&swrContext);av_frame_free(&picture);avcodec_close(codecContext);avformat_free_context(formatContext);env->ReleaseStringUTFChars(inputFilePath_, inputFilePath);return;}helper.registerCallback(playerCallback);helper.play();playerCallback(helper.bufferQueueItf, NULL);env->ReleaseStringUTFChars(inputFilePath_, inputFilePath);
}void release() {//释放资源av_free_packet(pkt);av_freep(out);out = NULL;swr_free(&swrContext);av_frame_free(&picture);avcodec_close(codecContext);avformat_free_context(formatContext);
}void getData(uint8_t **out, int *buff_size) {if (out == NULL || buff_size == NULL) {return;}int picture_ptr = 0;while (av_read_frame(formatContext, pkt) == 0) {//读到每一帧的压缩数据存放在AVPacketif (pkt->stream_index == audio_stream_idx) {//解码avcodec_decode_audio4(codecContext, picture, &picture_ptr, pkt);LOGE("picture_ptr %d", picture_ptr);if (picture_ptr > 0) {//转码swr_convert(swrContext, out, out_size,(const uint8_t **) (picture->data), picture->nb_samples);//缓冲区真实大小*buff_size = av_samples_get_buffer_size(NULL, channel_count, picture->nb_samples,AV_SAMPLE_FMT_S16, 1);break;}}}av_free_packet(pkt);
}/*** 播放器会不断调用此函数,我们需要在此回调中不断给缓冲区填充数据* @param bufferQueueItf * @param pContext */
void playerCallback(SLAndroidSimpleBufferQueueItf bq, void *pContext) {if (helper.playState == SL_PLAYSTATE_PLAYING) {getData(&out, &buff_size);if (out != NULL && buff_size != 0) {(*bq)->Enqueue(bq, out, (SLuint32) (buff_size));}} else if(helper.playState == SL_PLAYSTATE_STOPPED){release();helper.~SLresult();helper = NULL;}
}extern "C"
JNIEXPORT void JNICALL
Java_com_aruba_ffmpegapplication_PcmPlayActivity_stopByOpenSL(JNIEnv *env, jobject instance) {helper.stop();
}
最后不要忘了在cmake中添加刚刚的_opensl_helper类文件
经测试后,可以播放音频流了
项目地址:https://gitee.com/aruba/FFmpegApplication.git
http://www.taodudu.cc/news/show-4883416.html

相关文章:

  • 分析OpenSL回声Demo
  • matlab 图像尺寸 批量,matlab 图像批量修改图像大小
  • 批量更改图片大小程序
  • C++ opencv批量修改图片大小
  • php中怎样把图片改大,PHP怎么批量修改图片大小?
  • python批量修改图片内容_python批量修改图片大小的方法
  • 服务器无法获取header请求头参数
  • 关于向S3服务器上传中文名称文件失败的处理办法
  • 自建对象存储 minio 搭建和使用
  • 今天工作有用到,记录一下cloudberry 的基础用法吧
  • JDK开源镜像下载地址 一些国内常用的镜像站
  • [jdk]jdk7,jdk8,jdk14 linux版本,windows版本下载
  • java jdk 7_jdk1.7下载|Java Development Kit (JDK) 下载「64位」-太平洋下载中心
  • CentOS7两种方法安装 JDK
  • 如何成功抵御DOS攻击?给你介绍四种方法
  • Python 实现 DOS流量攻击
  • 针对应用层的DoS攻击
  • Slowloris dos攻击的原理及防护
  • 写轮眼常见的一些问题
  • #笔记
  • 内容分发平台的2018:头部阵营的三项总结,五大趋势
  • 华为mate20 pro Android,聊一聊华为Mate20pro的十个缺点——华为Mate20pro使用心得
  • 【ROM定制】Android 12 制作『MIUI官改』那点事①了解
  • Android“FakeID”签名漏洞分析和利用
  • android ifw 启动广告,应用控制器清爽无广告版-应用控制器官方最新版v1.9.5 免费版-腾牛安卓网...
  • 卷积神经网络案例:LeNet-5手写数字识别
  • 深度学习——手写数字识别底层实现
  • 用Numpy读取MNIST数据集(附已经读取完成的mat文件)
  • 将MNIST手写数字数据集导入NumPy数组(《深度学习入门:基于Python的理论与实现》实践笔记)
  • 【图像分类经典网络 | LeNet-5】一切都在孕育之中

NDK--利用OpenSL ES实现播放FFmpeg解码后的音频流相关推荐

  1. iOS 音视频开发:Audio Unit播放FFmpeg解码的音频

    本文档描述了iOS播放经FFmpeg解码的音频数据的编程步骤,具体基于Audio Toolbox框架的Audio Session和Audio Unit框架提供的接口实现.在iOS 7及以上平台Audi ...

  2. 【Android音视频】OpenSL ES音频播放示例一

    本文将实现一个使用OpenSL ES来播放assets目录下mp3歌曲的demo(实际推荐大家使用oboe库). Android NDK之高性能音频https://developer.android. ...

  3. OpenHarmony OpenSl ES音频播放开发

    1.OpenHarmony OpenSl ES音频播放简介 开发者可以通过本博文了解在OpenHarmony中如何使用OpenSL ES接口进行音频播放相关操作:当前仅实现了部分OpenSL ES接口 ...

  4. Android FFmpeg开发(三),利用OpenSL ES实现音频渲染

    上篇文章我们利用FFmpeg+ANativeWindwo实现了视频的解码和渲染,已经完成视频画面在SurfaceView上显示.还没阅读上一篇文章的同学建议先阅读:Android FFmpeg开发(二 ...

  5. 播放器基础--OpenSL ES音频播放

    介绍 官网 OpenSL ES (Open Sound Library for Embedded Systems)是针对嵌入式系统的一套无授权费,跨平台, 硬件加速的音频API.它提供了一套标准化,高 ...

  6. 用surfaceview播放FFmpeg解码视屏

    关于FFmpeg解码请看第一篇教程:FFmpeg解码 下载转码库libyuv 一般我们用surfaceview播放视频都是才用RGBA格式等播放的,但我们解码之后的视频可能是h.264等等 所以我们这 ...

  7. ffmpeg解码后图像呈绿色

    / 偶然发现 网站: 智城 让外包更简单~   有类似项目需求,可参考 http://www.taskcity.com/pages/search_projects_advance?keywords=& ...

  8. Ffmpeg + OpenSL ES + Android 播放音频

    一 拿到音频的 路径 ,然后根据 jni 发送数据到C语言的方法 然后,开始解析 avcodec_register_all();avformat_network_init();pPlayer-> ...

  9. Android通过OpenSL ES播放音频套路详解

    我的视频课程(基础):<(NDK)FFmpeg打造Android万能音频播放器> 我的视频课程(进阶):<(NDK)FFmpeg打造Android视频播放器> 我的视频课程(编 ...

最新文章

  1. python学习--第三天 粗略介绍人脸识别
  2. 上汽集团金忠孝: 人工智能时代的汽车将发生颠覆的变革
  3. 查看Linux内核及发行商版本命令
  4. 图片验证码的JAVA工具类
  5. docker容器的标准使用过程_跟我一起学docker(四)--容器的基本操作
  6. TabControl控件用法图解
  7. 【java】System.getProperty()参数大全
  8. Android 指纹调试流程(高通、MTK均适用)
  9. python 列表间隔取值_python list数据等间隔抽取并新建list存储的例子
  10. oracle 触发器 insert 前检查_一文看懂INSTEAD OF 与AFTER 触发器区别与联系
  11. LeetCode199. Binary Tree Right Side View
  12. 借贷宝java_【人人行(借贷宝)Java面试】借贷宝java后端开发面经。-看准网
  13. PS替换图片图标操作
  14. 接口测试之发包工具介绍
  15. C#设计模式系列:抽象工厂模式(AbstractFactory)
  16. Seaweedfs安装配置使用及mount挂载
  17. 1.2GHz Atom处理器 诺基亚N9配置曝光
  18. Python视频剪辑Auto-Editor一键预处理口播无声片段
  19. OBS studio黑屏解决办法
  20. 二分查找法和Fibonacci查找

热门文章

  1. 认识C++(引别人的)
  2. 店盈通:如何凭借自播在竞争激烈的速食品牌中突围?
  3. Background Matting视频抠图
  4. JavaScript二级联动
  5. AVR系列单片机的基本架构(翻译自俄语讲义)
  6. Android模仿360动态悬浮窗,像360悬浮窗那样,用WindowManager实现炫酷的悬浮迷你音乐盒(下)...
  7. 基于AI的语音信号处理技术
  8. 天线发射功率计算公式_天线功率详细说明
  9. Java学习路线总结(2022版)
  10. 编程之类的文案_有什么让人瞬间充满希望的文案?