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

Android音频采集

在 Android 开发领域,音频采集是一项非常重要且有趣的功能。它为各种应用程序,如语音聊天、音频录制、多媒体内容创作等提供了基础支持。今天我们就来深入探讨一下 Android 音频采集的两大类型:Mic 音频采集和系统音频采集。

1. Mic音频采集

在 Android 中,我们通常使用 AudioRecord 类来实现 Mic 音频采集。

1.1 AudioRecord介绍

1.1.1 参数介绍

@RequiresPermission(android.Manifest.permission.RECORD_AUDIO)
public AudioRecord(int audioSource, int sampleRateInHz, int channelConfig, int audioFormat,int bufferSizeInBytes)
  1. 音频源(audioSource)

    • 这个参数指定了音频数据的来源。例如,MediaRecorder.AudioSource.MIC表示从设备的麦克风获取音频。除此之外,还有其他可选的音频源,如VOICE_RECOGNITION(用于语音识别)、VOICE_COMMUNICATION(用于语音通信,例如 VoIP 应用)等。不同的音频源在音频采集的特性上可能会有所不同,例如针对语音通信的音频源可能会对音频进行一些预处理,以优化语音传输的质量。
  2. 采样率(sampleRateInHz)

    • 采样率决定了每秒从音频信号中采集的样本数量。常见的采样率有 44100Hz(44.1kHz)和 16000Hz(16kHz)等。较高的采样率可以提供更高质量的音频,但同时也会产生更大的数据量。例如,44.1kHz 的采样率常用于音乐录制等对音质要求较高的场景,而 16kHz 的采样率在语音通信等场景中较为常见,因为它在保证一定语音清晰度的前提下,能够减少数据传输和处理的负担。
  3. 声道配置(channelConfig)

    • 声道配置参数用于指定音频是单声道(AudioFormat.CHANNEL_IN_MONO)还是立体声(AudioFormat.CHANNEL_IN_STEREO)。单声道音频只有一个音频通道,而立体声有两个通道,分别对应左右声道。在移动设备上,考虑到性能和存储空间等因素,单声道采集较为常用,并且可以在后期通过算法将单声道转换为立体声,以满足不同的应用需求。
  4. 音频格式(audioFormat)

    • 音频格式参数确定了音频数据的编码格式。常见的有AudioFormat.ENCODING_PCM_16BIT(16 位)和AudioFormat.ENCODING_PCM_8BIT(8 位)等。16 位格式能够提供更丰富的音频动态范围和更好的音质,但数据量相对较大。在 Android 手机等设备上,16 位 PCM 格式具有较好的兼容性,是比较常用的音频格式。
  5. 缓冲区大小(bufferSizeInBytes)

    • 缓冲区大小是AudioRecord中一个非常关键的参数。它决定了在音频采集过程中用于存储音频数据的缓冲区的大小。合适的缓冲区大小可以确保音频采集的流畅性,避免出现数据丢失或音频卡顿等问题。可以通过AudioRecord.getMinBufferSize方法来获取满足指定音频参数(采样率、声道配置和音频格式)的最小缓冲区大小。这个方法会根据设备的硬件性能和音频参数计算出一个合适的值,开发者通常可以根据这个最小值来合理设置缓冲区大小,例如可以适当增大缓冲区大小以应对一些复杂的音频处理场景,但过大的缓冲区可能会导致音频采集的延迟增加。

1.1.2 工作流程

  1. 初始化

    • 首先,需要使用合适的音频参数来创建AudioRecord对象。通过调用AudioRecord的构造函数,传入音频源、采样率、声道配置、音频格式和缓冲区大小等参数来完成初始化。例如:
int bufferSize = AudioRecord.getMinBufferSize(mSampleRate, channelConfig, mAudioFormat);if (bufferSize == AudioRecord.ERROR || bufferSize == AudioRecord.ERROR_BAD_VALUE) {Log.e(TAG, "AudioRecord.getMinBufferSize failed: " + bufferSize);return;
}mBuffer = new byte[bufferSize];
mAudioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, mSampleRate, channelConfig, mAudioFormat, bufferSize);
  • 如果audioRecord.getState() == AudioRecord.STATE_INITIALIZED,则表示AudioRecord对象成功初始化,可以进行下一步操作。
  1. 开始采集

    • 调用audioRecord.startRecording()方法来启动音频采集。一旦开始采集,音频数据就会开始填充到缓冲区中。
  2. 读取数据

    • 通常会在一个单独的线程中读取缓冲区中的音频数据。可以使用audioRecord.read()方法来读取数据。例如,将读取的数据存储到一个byte类型的数组中:
int bytesRead = mAudioRecord.read(mBuffer, 0, mBuffer.length);
  • 这个readSize表示实际读取到的音频数据的大小。需要注意的是,在读取数据的过程中,要及时处理数据,避免缓冲区溢出。
  1. 停止采集和释放资源

    • 当音频采集完成后,需要调用audioRecord.stop()方法来停止采集,然后调用audioRecord.release()方法来释放AudioRecord对象占用的资源。这一步非常重要,因为如果不释放资源,可能会导致内存泄漏等问题。

1.2 具体实现

1.2.1 MainActivity

申请权限并打开CaptureActivity

package com.skystack.mediaexporation;import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;import android.Manifest;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.TextView;
import android.widget.Toast;import com.skystack.mediaexporation.databinding.ActivityMainBinding;public class MainActivity extends AppCompatActivity {// Used to load the 'mediaexporation' library on application startup.static {System.loadLibrary("mediaexporation");}private ActivityMainBinding binding;private final static String TAG = MainActivity.class.getName();static private final String[] PERMISSION = {Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.RECORD_AUDIO};private final static int RequestCodePermissions = 1;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);binding = ActivityMainBinding.inflate(getLayoutInflater());setContentView(binding.getRoot());if (!CheckPermission()) {ActivityCompat.requestPermissions(this, PERMISSION, RequestCodePermissions);}else{Init();}}private boolean CheckPermission(){boolean ret = true;for (String str : PERMISSION) {ret = ret && (ActivityCompat.checkSelfPermission(this, str) == PackageManager.PERMISSION_GRANTED);}return ret;}@Overridepublic void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {super.onRequestPermissionsResult(requestCode, permissions, grantResults);if (requestCode == RequestCodePermissions) {if (!CheckPermission()) {Log.e(TAG, "request permissions denied");Toast.makeText(this, "request permissions denied", Toast.LENGTH_SHORT).show();finish();}else{Init();}}}private void Init(){}@Overridepublic boolean onCreateOptionsMenu(Menu menu) {getMenuInflater().inflate(R.menu.main, menu);return true;}@Overridepublic boolean onOptionsItemSelected(@NonNull MenuItem item) {switch (item.getItemId()){case R.id.capture:Toast.makeText(this, "capture", Toast.LENGTH_SHORT).show();CaptureActivity.IntentTo(this);break;case R.id.setting:Toast.makeText(this, "setting", Toast.LENGTH_SHORT).show();break;}return super.onOptionsItemSelected(item);}
}

1.2.2 CaptureActivity

采集控制及回调

package com.skystack.mediaexporation;import androidx.appcompat.app.AppCompatActivity;import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.os.Environment;
import android.util.Log;
import android.view.View;
import android.widget.Button;import com.skystack.mediaexporation.databinding.ActivityCaptureBinding;import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;public class CaptureActivity extends AppCompatActivity implements AudioCapture.AudioCaptureCallback{private static final String TAG = CaptureActivity.class.getName();private ActivityCaptureBinding binding;private AudioCapture mAudioCapture;private File mAudioFile;private FileOutputStream mAudioOutputStream;public static Intent NewIntent(Context context){Intent intent = new Intent(context, CaptureActivity.class);return intent;}public static void IntentTo(Context context){context.startActivity(NewIntent(context));}@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);binding = ActivityCaptureBinding.inflate(getLayoutInflater());setContentView(binding.getRoot());mAudioCapture = new AudioCapture(this, 44100, 2);binding.buttonStart.setOnClickListener(new View.OnClickListener() {boolean mIsCapture = false;@Overridepublic void onClick(View v) {mIsCapture = !mIsCapture;if(mIsCapture){if(mAudioFile == null) {mAudioFile = new File(Environment.getExternalStorageDirectory(), "audio.pcm");}if(mAudioOutputStream == null){try {mAudioOutputStream = new FileOutputStream(mAudioFile);} catch (FileNotFoundException e) {e.printStackTrace();}}mAudioCapture.StartRecord();binding.buttonStart.setText("停止采集");}else{mAudioCapture.StopRecording();binding.buttonStart.setText("采集音频");if(mAudioFile != null){try {mAudioOutputStream.close();} catch (IOException e) {e.printStackTrace();}}}}});}@Overridepublic void OnAudioDataAvailable(byte[] data) {Log.i(TAG, "OnAudioDataAvailable: " + data.length);if(mAudioOutputStream != null){try {mAudioOutputStream.write(data);} catch (IOException e) {e.printStackTrace();}}}
}

1.2.3 AudioCapture

音频采集类

package com.skystack.mediaexporation;import android.annotation.SuppressLint;
import android.media.AudioFormat;
import android.media.AudioRecord;
import android.media.MediaRecorder;
import android.util.Log;public class AudioCapture {private static final String TAG = AudioCapture.class.getName();private final int mSampleRate;private final int mChannels;private static int mAudioFormat = AudioFormat.ENCODING_PCM_16BIT;private AudioRecord mAudioRecord;private AudioCaptureCallback mCallback;private AudioRecordThread mRecordThread = null;private byte[] mBuffer = null;public AudioCapture(AudioCaptureCallback callback, int mSampleRate, int mChannels) {this.mCallback = callback;this.mSampleRate = mSampleRate;this.mChannels = mChannels;InitCapture();}private int ChannelCountToConfiguration(int channels) {return (channels == 1 ? android.media.AudioFormat.CHANNEL_IN_MONO : android.media.AudioFormat.CHANNEL_IN_STEREO);}@SuppressLint("MissingPermission")public void InitCapture() {int channelConfig = ChannelCountToConfiguration(mChannels);int bufferSize = AudioRecord.getMinBufferSize(mSampleRate, channelConfig, mAudioFormat);if (bufferSize == AudioRecord.ERROR || bufferSize == AudioRecord.ERROR_BAD_VALUE) {Log.e(TAG, "AudioRecord.getMinBufferSize failed: " + bufferSize);return;}mBuffer = new byte[bufferSize];mAudioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, mSampleRate, channelConfig, mAudioFormat, bufferSize);if (mAudioRecord == null || mAudioRecord.getState() != AudioRecord.STATE_INITIALIZED) {Log.e(TAG,"Failed to create a new AudioRecord instance");ReleaseAudioResources();return;}}public boolean StartRecord(){if(mAudioRecord == null) return false;if(mRecordThread != null) return false;try {mAudioRecord.startRecording();} catch (IllegalStateException e) {Log.e(TAG,"AudioRecord.startRecording failed: " + e.getMessage());return false;}if (mAudioRecord.getRecordingState() != AudioRecord.RECORDSTATE_RECORDING) {Log.e(TAG, "AudioRecord.startRecording failed - incorrect state :"+ mAudioRecord.getRecordingState());return false;}mRecordThread = new AudioRecordThread();mRecordThread.start();return true;}public boolean StopRecording() {Log.d(TAG, "stopRecording");if(mRecordThread == null)return true;mRecordThread.StopThread();try {mRecordThread.join(2000);} catch (InterruptedException e) {e.printStackTrace();}mRecordThread = null;Log.d(TAG, "stopRecording done");return true;}public boolean DestroyRecording(){StopRecording();ReleaseAudioResources();return true;}private void ReleaseAudioResources() {Log.d(TAG, "releaseAudioResources");if (mAudioRecord != null) {mAudioRecord.release();mAudioRecord = null;}if(mBuffer != null){mBuffer = null;}}private class AudioRecordThread extends Thread {private volatile boolean mKeepAlive = true;@Overridepublic void run() {while (mKeepAlive){int bytesRead = mAudioRecord.read(mBuffer, 0, mBuffer.length);if(bytesRead == mBuffer.length){if(mCallback != null){mCallback.OnAudioDataAvailable(mBuffer);}} else {String errorMessage = "AudioRecord.read failed: " + bytesRead;Log.e(TAG, errorMessage);if (bytesRead == AudioRecord.ERROR_INVALID_OPERATION) {mKeepAlive = false;Log.e(TAG, errorMessage);}}}try {if(mAudioRecord != null){mAudioRecord.stop();}}catch (IllegalStateException e){Log.e(TAG, "AudioRecord.stop failed: " + e.getMessage());}}public void StopThread() {Log.d(TAG, "stopThread");mKeepAlive = false;}}public interface AudioCaptureCallback{void OnAudioDataAvailable(byte[] data);}}

2. 系统音频采集

系统音频采集有两种方法,但都有局限性。

2.1 REMOTE_SUBMIX

将AudioRecord的source设置为REMOTE_SUBMIXREMOTE_SUBMIX会截断麦克风和耳机的声音,通过AudioRecord采集输出。

但是REMOTE_SUBMIX需要有system权限。适用于自己编的系统中采集音频,一般用在云手机等场景。

获取系统权限步骤:

  1. 在AndroidManifest.xml中声明系统权限,同时申请CAPTURE_AUDIO_OUTPUT权限。
android:sharedUserId="android.uid.system"
<uses-permission android:name="android.permission.CAPTURE_AUDIO_OUTPUT" />
  1. 生成APK时,用与Android系统源码编译时一致的签名文件。

2.2 AudioPlaybackCapture

AudioPlaybackCapture API 是在 Android 10 中引入的。应用可以借助此 API 复制其他应用正在播放的音频。此功能类似于屏幕截图,但针对的是音频。主要用例是视频在线播放应用,这些应用希望捕获游戏正在播放的音频。

2.2.1 构建捕获应用

出于安全和隐私考虑,捕获播放的音频会施加一些限制。为了能够捕获音频,应用必须满足以下要求:

  • 应用必须具有 RECORD_AUDIO权限。
  • 应用必须调出 MediaProjectionManager.createScreenCaptureIntent() 显示的提示,并且用户必须批准此提示。
  • 捕获和播放音频的应用必须使用同一份用户个人资料。

如要从其他应用中捕获音频,您的应用必须构建AudioRecord 对象,并向其添AudioPlaybackCaptureConfiguration。请按以下步骤操作:

  1. 调用 AudioPlaybackCaptureConfiguration.Builder.build()以构建AudioPlaybackCaptureConfiguration
  2. 通过调用 setAudioPlaybackCaptureConfig将配置传递给 AudioRecord
AudioFormat audioFormat = new AudioFormat.Builder().setChannelMask(channelConfig).setSampleRate(mSampleRate).setEncoding(mAudioFormat).build();AudioPlaybackCaptureConfiguration configuration =new AudioPlaybackCaptureConfiguration.Builder(mediaProjection).addMatchingUsage(AudioAttributes.USAGE_MEDIA).addMatchingUsage(AudioAttributes.USAGE_GAME).addMatchingUsage(AudioAttributes.USAGE_UNKNOWN).build();mAudioRecord = new AudioRecord.Builder().setAudioPlaybackCaptureConfig(configuration).setAudioFormat(audioFormat).setBufferSizeInBytes(bufferSize).build();

2.2.2 控制音频捕获

您的应用可以控制它可以录制的内容类型,以及哪些其他类型的应用可以录制自己的播放。

应用可以使用以下方法限制其可以捕获的音频:

  • AUDIO_USAGE 传递给AudioPlaybackCaptureConfiguration.addMatchingUsage()可允许捕获特定用法。多次调用该方法可指定多个用法。
  • AUDIO_USAGE 传递给 AudioPlaybackCaptureConfiguration.excludeUsage() 可禁止捕获相应用法。多次调用该方法可指定多个用法。
  • 将 UID 传递到 AudioPlaybackCaptureConfiguration.addMatchingUid()可仅捕获具有特定 UID 的应用。多次调用该方法可指定多个 UID。
  • 将 UID 传递到 AudioPlaybackCaptureConfiguration.excludeUid()可禁止捕获相应 UID。多次调用该方法可指定多个 UID。

请注意,您不能同时使用 addMatchingUsage()excludeUsage() 方法。您必须选择其中之一。同样,您也不能同时使用 addMatchingUid()excludeUid()

2.2.3 获取MediaProjection

首先注册一个ActivityResultLauncher

mLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(),new ActivityResultCallback<ActivityResult>() {@Overridepublic void onActivityResult(ActivityResult result) {if(result.getResultCode() == RESULT_OK){if(result.getData() != null){mAudioCaptureIntent = new Intent(captureActivity, AudioCaptureService.class);mAudioCaptureIntent.putExtra("resultCode", result.getResultCode());mAudioCaptureIntent.putExtra("data", result.getData());if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {startForegroundService(mAudioCaptureIntent);}Log.i(TAG, "获取屏幕录制权限成功");binding.buttonMedia.setText("停止采集");}}else{Log.e(TAG, "获取屏幕录制权限失败");binding.buttonMedia.setText("采集媒体");}}}
);

在开始采集时启动createScreenCaptureIntent

MediaProjectionManager mediaProjectionManager = (MediaProjectionManager) getSystemService(MEDIA_PROJECTION_SERVICE);
Intent screenCaptureIntent = mediaProjectionManager.createScreenCaptureIntent();
mLauncher.launch(screenCaptureIntent);

获取MediaProjection

注意:

  1. mediaProjection需要一个前台服务,所以获取MediaProjection需要在一个前台服务中运行,我们创建了一个前台service AudioCaptureService
  2. AndroidManifest.xml中的service中声明AudioCaptureService为类型为mediaProjection的前台服务。
<serviceandroid:name=".Capture.AudioCaptureService"android:foregroundServiceType="mediaProjection"android:enabled="true"android:exported="true"></service>
  1. 获取MediaProjection必须在startForeground之后。

3. Audacity播放

  1. 先从Andorid设备中导出保存的audio.pcm文件到Windows上。
  2. 打开Audacity。
  3. 文件->导入->原始数据,并选择audio.pcm文件。
  4. 按照采集时设置的格式,设置播放格式。
  5. 导入并播放。

Audacity

4 完整代码

github

相关文章:

Android音频采集

在 Android 开发领域&#xff0c;音频采集是一项非常重要且有趣的功能。它为各种应用程序&#xff0c;如语音聊天、音频录制、多媒体内容创作等提供了基础支持。今天我们就来深入探讨一下 Android 音频采集的两大类型&#xff1a;Mic 音频采集和系统音频采集。 1. Mic音频采集…...

通过轻易云平台实现聚水潭数据高效集成到MySQL的技术方案

聚水潭数据集成到MySQL的技术案例分享 在本次技术案例中&#xff0c;我们将详细探讨如何通过轻易云数据集成平台&#xff0c;将聚水潭的数据高效、可靠地集成到MySQL数据库中。具体方案为“聚水谭-店铺查询单-->BI斯莱蒙-店铺表”。这一过程不仅需要处理大量数据的快速写入…...

类和对象( 中 【补充】)

目录 一 . 赋值运算符重载 1.1 运算符重载 1.2 赋值运算符重载 1.3 日期类实现 1.3.1 比较日期的大小 : 1.3.2 日期天数 : 1.3.3 日期 - 天数 : 1.3.4 前置/后置 1.3.5 日期 - 日期 1.3.6 流插入 << 和 流提取 >> 二 . 取地址运算符重载 2.1 const…...

如何使用gpio模拟mdio通信?

一、前言 实际项目开发中&#xff0c;由于设计原因&#xff0c;会将phy的mdio引脚连接到SoC的2个空闲gpio上&#xff0c; 这样就无法通过Gmac自有的架构实现修改phy&#xff0c; 因此只能通过GPIO模拟的方式实现MDIO&#xff0c; 好在Linux支持MDIO via GPIO功能。 该功能…...

C# 中的事件和委托:构建响应式应用程序

C#中的事件和委托。事件和委托是C#中用于实现观察者模式和异步回调的重要机制&#xff0c;它们在构建响应式和交互式应用程序中发挥着重要作用。以下是一篇关于C#中事件和委托的文章。 引言 事件和委托是C#语言中非常重要的特性&#xff0c;它们允许你实现观察者模式和异步回…...

科技赋能健康:多商户Java版商城系统引领亚健康服务数字化变革

在当今社会&#xff0c;随着生活节奏的加快和工作压力的增大&#xff0c;越来越多的人处于亚健康状态。据《The Lancet》期刊2023年的统计数据显示&#xff0c;全球亚健康状态的人群比例已高达82.8%&#xff0c;这一数字背后&#xff0c;隐藏着巨大的健康风险和社会成本。亚健康…...

区块链网络示意图;Aura共识和Grandpa共识(BFT共识)

目录 区块链网络示意图 Aura共识和Grandpa共识(BFT共识) Aura共识 Grandpa共识(BFT共识) Aura与Grandpa的结合 区块链网络示意图 CP Blockchain:这是中央处理区块链(或可能指某种特定的处理单元区块链)的缩写。它可能代表了该区块链网络的主要处理或存储单元。在这…...

Javaweb梳理18——JavaScript

今日目标 掌握 JavaScript 的基础语法掌握 JavaScript 的常用对象&#xff08;Array、String&#xff09;能根据需求灵活运用定时器及通过 js 代码进行页面跳转能通过DOM 对象对标签进行常规操作掌握常用的事件能独立完成表单校验案例 18.1 JavaScript简介 JavaScript 是一门跨…...

面向对象-接口的使用

1. 接口的概述 为什么有接口&#xff1f; 借口是一种规则&#xff0c;对于继承而言&#xff0c;部分子类之间有共同的方法&#xff0c;为了约束方法的使用&#xff0c;使用接口。 接口的应用&#xff1a; 接口不是一类事物&#xff0c;它是对行为的抽象。 2. 接口的定义和使…...

失落的Apache JDBM(Java Database Management)

简介 Apache JDBM&#xff08;Java Database Management&#xff09;是一个轻量级的、基于 Java 的嵌入式数据库管理系统。它主要用于在 Java 应用程序中存储和管理数据。这个项目已经过时了&#xff0c;只是发表一下以示纪念&#xff0c;现在已经大多数被SQLite和Derby代替。…...

Vue3+SpringBoot3+Sa-Token+Redis+mysql8通用权限系统

sa-token支持分布式token 前后端代码&#xff0c;地球号: bright12389...

MySQL 三大日志详解

在 MySQL 数据库中&#xff0c;binlog&#xff08;二进制日志&#xff09;、redo log&#xff08;重做日志&#xff09;和 undo log&#xff08;回滚日志&#xff09;起着至关重要的作用。它们共同保障了数据库的高可用性、数据一致性和事务的可靠性。下面将对这三大日志进行详…...

Java 岗面试八股文及答案整理(2024最新版)

春招&#xff0c;秋招&#xff0c;社招&#xff0c;我们 Java 程序员的面试之路&#xff0c;是挺难的&#xff0c;过了 HR&#xff0c;还得被技术面&#xff0c;小刀在去各个厂面试的时候&#xff0c;经常是通宵睡不着觉&#xff0c;头发都脱了一大把&#xff0c;还好最终侥幸能…...

Web3.0安全开发实践:Clarity最佳实践总结

在过去的一段时间里&#xff0c;CertiK团队对比特币生态系统及其发展进行了深入研究。同时&#xff0c;团队还审计了多个比特币项目以及基于不同编程语言的智能合约&#xff0c;包括OKX的BRC-20钱包和MVC DAO的sCrypt智能合约实现。 现在&#xff0c;我们的研究重点转向了Clar…...

基于Springboot+Vue动漫推荐平台管理系统(源码+lw+讲解部署+PPT)

前言 详细视频演示 论文参考 系统介绍 系统概述 核心功能 用户角色与功能 具体实现截图 1. 热门动漫功能 2. 文章专栏功能 3. 会员分享功能 4. 热门动漫管理功能&#xff08;管理员端&#xff09; 5. 动漫分类管理功能 技术栈 后端框架SpringBoot 前端框架Vue …...

秋意浓,森林披金装

秋意浓&#xff0c;森林披金装&#xff0c; 枫叶如火&#xff0c;漫山遍野狂。 松间轻风送寒意&#xff0c; 鸟鸣悠扬入云翔。 林间小径蜿蜒行&#xff0c; 落叶铺成金色毯。 溪水潺潺绕石转&#xff0c; 映出天边一抹霞。 野菊点缀在草间&#xff0c; 白云悠悠随意闲。…...

Chrome离线安装包下载

1、问Chrome的官网&#xff1a;https://www.google.cn/chrome/ 直接下载的是在线安装包&#xff0c;安装需要联网。 2、如果需要在无法联网的设备上安装Chrome&#xff0c;需要在上面的地址后面加上?standalone1。 Chrome离线安装包下载地址&#xff1a;https://www.google.c…...

安卓手机5G网络频繁掉4G 问题解决 手机5G网络优化方案

问题环境 在某个长期停留的位置&#xff08;例如&#xff1a;躺平&#xff09;使用手机时网络突然从5G跳到4G&#xff0c;偶尔跳来跳去导致网络体验很差&#xff0c;经过调整5G网络情况下网速及其他体验都要更好&#xff0c;基于这样的情况使用一种简单的操作&#xff0c;锁定5…...

使用LLaMA-Factory微调时的问题与解决方案记录

文章目录 如何指定微调使用的显卡如何解决显卡通信导致的报错模型微调的实际epoch和step如何计算如何实现多卡全量微调模型微调后的结果如何查看模型测试后的指标如何理解如何指定微调使用的显卡 启动网页时使用这种执行命令 CUDA_VISIBLE_DEVICES=5,6,7 llamafactory-cli we…...

Go语言switch语句

在Go语言中&#xff0c;switch&#xff0c;是一个高度灵活&#xff0c;其功能强大的控制结构&#xff0c;相比较Java中的switch&#xff0c;更受到语言重视。 目录 1.基础用法2.多值匹配3.不指定表达式的 switch4.使用 fallthrough 强制进入下一个分支5.使用类型断言的 switch…...

JavaScript DOM使用

DOM Document Object Model 简单而言&#xff0c;就是JavaScript将HTML文档的各个组成部分封装为对象。 封装的对象分别为&#xff1a; Document&#xff1a;整个HTML的文档对象 Element&#xff1a;元素对象&#xff08;也就是HTML中的标签&#xff09; Attribute&#xff1a;…...

人工智能|计算机视觉——微表情识别(Micro expression recognition)的研究现状

一、简述 微表情是一种特殊的面部表情,与普通的表情相比,微表情主要有以下特点: 持续时间短,通常只有1/25s~1/3s;动作强度低,难以察觉;在无意识状态下产生,通常难以掩饰或伪装;对微表情的分析通常需要在视频中,而普通表情在图像中就可以分析。由于微表情在无意识状态…...

耿恭坚守城池的方法

疏勒城之战中&#xff0c;耿恭坚守城池的方法主要有以下几点&#xff1a; 选择有利地势&#xff1a;耿恭深知疏勒城依山傍水、地势险要&#xff0c;易守难攻&#xff0c;于是果断放弃金满城&#xff0c;移师至疏勒城据守&#xff0c;为长期坚守创造了良好的地理条件.运用心理战…...

小兔鲜项目总结——项目亮点

目录 1、基于业务的逻辑组件拆分思想2、长页面吸顶交互的实现3、自定义图片懒加载指令并封装为插件4、画板插槽组件等业务通用组件封装5、通用逻辑函数的封装6、列表无限加载7、路由缓存问题的处理 小兔鲜项目其实在暑假之前就已经做完了&#xff0c;但是一直没有空做总结&…...

Cesium的ClearCommand的流程

ClearCommand是在每帧渲染前可以将显存的一些状态置为初始值&#xff0c;就如同把擦黑板。当然也包括在绘制过程中擦掉部分的数据&#xff0c;就如同画家在开始绘制的时候会画导览线&#xff08;如透视线&#xff09;&#xff0c;轮廓出来后这些导览线就会被擦除。 我画了一个…...

Fakelocation Server服务器/专业版 ubuntu

前言:需要Ubuntu系统 Fakelocation开源文件系统需求 Ubuntu | Fakelocation | 任务一 任务一 更新Ubuntu&#xff08;安装下载不再赘述&#xff09; sudo -i # 提权 sudo apt update # 更新软件包列表 sudo apt upgrade # 升级已安装的软…...

Spring AI Alibaba 快速入门

Spring AI Alibaba 实现了与阿里云通义模型的完整适配&#xff0c;接下来&#xff0c;我们将学习如何使用 spring ai alibaba 开发一个基于通义模型服务的智能聊天应用。 一、快速体验示例 注意&#xff1a;因为 Spring AI Alibaba 基于 Spring Boot 3.x 开发&#xff0c;因此…...

Docker Registry(镜像仓库)详解

Docker Registry&#xff08;镜像仓库&#xff09;详解 Docker Registry&#xff0c;即Docker镜像仓库&#xff0c;是Docker生态系统中一个至关重要的组件。它负责存储、管理和分发Docker镜像&#xff0c;为Docker容器提供镜像资源。本文将深入探讨Docker Registry的功能、结构…...

RTOS学习笔记---“二值信号量”和“互斥信号量”

在实时操作系统&#xff08;RTOS&#xff09;中&#xff0c;“二值信号量”和“互斥信号量”是两种常见的同步机制&#xff0c;用于线程之间的协调与资源管理。尽管它们有相似之处&#xff0c;都基于信号量概念&#xff0c;但它们的用途和行为存在重要区别。 1. 二值信号量&…...

Oracle-物化视图基本操作

-- 物化视图 -- 与普通视图的区别&#xff1a;真实存在数据的 普通视图的数据在基表 物化视图看成是, 一个定时运行的计算JOB一个存计算结果的表 创建时生成数据&#xff1a; 分为两种&#xff1a;build immediate 和 build deferred&#xff0c; build immediate是在创…...