Android LAME原生音频

news2024/7/6 19:55:14

前言

我想大家都做过录音的功能吧,首先想到的是不是MediaRecorder?今天我们不用MediaRecorder,而是使用LAME库自己编译音频编码模块,很明显,这个需要用到NDK。凡是涉及到音视频编解码这块的,都需要用到Android NDK(Native Development Kit),原生开发工具包。即使用C/C++代码实现音频的采样和编码,然后使用Java去调用原生模块实现录音功能。

音视频相关基础知识

我来简单过一下基础的音视频相关知识。

Audio Sample:音频采样,通常指录制音频采样文件PCM的过程,PCM(脉冲编码调制)是一种音频流,称为裸流。人耳听到的是模拟信号,PCM是把声音从模拟信号转化为数字信号的技术。

Audio Track:音轨,封装格式的音频文件通常由多个音轨组成,比如MP3文件就是一种封装格式的音频文件,与之相对的就是音频原始采样数据PCM。比如一首歌,歌声是一个音轨,吉他声、鼓声等一些混合在其中的声音各自也是一个音轨。

Audio Channel:声道,比如左声道、右声道和环绕立体声。

Bitrate:比特率,俗称码率。它直接决定声音的清晰度即声音特征的详细程度,SQ、HQ音质是通过它来判断的,码率越高,音频文件越大,质量也越高。

Sample Rate:采样率,大多数沿用国际通用的标准采样率,即44.100kHZ或者48.000kHZ,肯定是不能录制超声波和次声波的,因为人耳感知不到。

录音和播放声音的详细过程。

录音:音频采样编码->音频封装

播放声音:音频解封装->音频解码播放

录音首先采样并编码得到PCM文件,然后封装PCM文件为MP3、WAV、FLAC等文件,播放声音首先也要解封装成PCM文件,然后对PCM文件解码播放。

编译共享库so

编译共享库so的过程有两种方式,一种是使用ndk-build+Android.mk+Application.mk,一种是CMake+CMakeLists.txt。今天我们采用最原始的方式,mk文件。其实Android.mk和CMakeLists.txt很多东西是一一对应的。

Android.mkCMakeLists.txt
LOCAL_MODULE、LOCAL_SRC_FILESadd_library
LOCAL_CFLAGSadd_definitions
LOCAL_C_INCLUDESinclude_directories
LOCAL_STATIC_LIBRARIES、LOCAL_SHARED_LIBRARIESadd_library + set_target_properties
LOCAL_LDLIBSfind_library
C文件

截屏2023-09-13 14.40.51.png

Android.mk
LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LAME_LIBMP3_DIR := lame-3.100_libmp3lame

LOCAL_MODULE    := mp3lame

LOCAL_SRC_FILES :=\
$(LAME_LIBMP3_DIR)/bitstream.c \
$(LAME_LIBMP3_DIR)/fft.c \
$(LAME_LIBMP3_DIR)/id3tag.c \
$(LAME_LIBMP3_DIR)/mpglib_interface.c \
$(LAME_LIBMP3_DIR)/presets.c \
$(LAME_LIBMP3_DIR)/quantize.c \
$(LAME_LIBMP3_DIR)/reservoir.c \
$(LAME_LIBMP3_DIR)/tables.c  \
$(LAME_LIBMP3_DIR)/util.c \
$(LAME_LIBMP3_DIR)/VbrTag.c \
$(LAME_LIBMP3_DIR)/encoder.c \
$(LAME_LIBMP3_DIR)/gain_analysis.c \
$(LAME_LIBMP3_DIR)/lame.c \
$(LAME_LIBMP3_DIR)/newmdct.c \
$(LAME_LIBMP3_DIR)/psymodel.c \
$(LAME_LIBMP3_DIR)/quantize_pvt.c \
$(LAME_LIBMP3_DIR)/set_get.c \
$(LAME_LIBMP3_DIR)/takehiro.c \
$(LAME_LIBMP3_DIR)/vbrquantize.c \
$(LAME_LIBMP3_DIR)/version.c \
MP3Encoder.c

include $(BUILD_SHARED_LIBRARY)
  • LOCAL_PATH :=$(call my-dir)
    当前文件在系统中的路径,必须为Android.mk文件的第一行。
  • include $(CLEAR_VARS)
    清除上一次构建中的全局变量,开始一次新的编译。
  • LOCAL_MODULE
    生成的模块的名称,这里是动态库so文件的名称,so文件的名称拼接为lib「模块名」.so
    该模块的编译的目标名,用于区分各个模块,名字必须是唯一并不包含空格的,如果编译目标是 so 库,那么该 so 库的名称就是 lib 项目名 .so。
  • LOCAL_SRC_FILES
    要编译的.c或.cpp文件,.h和.hpp文件可以不用出现在这里,系统会自动包含。
  • include $(BUILD_SHARED_LIBRARY)
    include开头的是构建系统的内置变量,此行代码的意思是构建动态库,也称共享库,还有以下几种取值。
    BUILD_STATIC_LIBRARY: 构建静态库。
    PREBUILT_STATIC_LIBRARY: 将静态库包装成一个模块。
    PREBUILT_SHARED_LIBRARY: 将静态库包装成一个模块。
    BUILD_EXECUTABLE: 构建可执行文件。
Application.mk
APP_ABI := armeabi  armeabi-v7a  arm64-v8a  x86  x86_64  mips  mips64
APP_MODULES := mp3lame
APP_CFLAGS += -DSTDC_HEADERS
APP_PLATFORM := android-21
  • APP_ABI ABI(Application Binary Interface)应用二级制接口,这是一种计算机科学中的概念,用于描述软件库或操作系统与应用程序之间的二进制通信方式。它跟CPU指令集对应。
  • APP_MODULES 指定模块
  • APP_FLAGS 指定编译过程的flag,“DSTDC_HEADERS” 是一个编程中常见的宏定义,通常用于检查标准库头文件是否已经包含。这个宏定义通常在C/C++代码中使用,用于确保标准库的头文件已经正确包含,以便程序可以正常编译和运行。如果没有正确包含标准库头文件,编译器可能会报错或者出现未定义的行为。
  • APP_PLATFORM 指定创建的动态库的平台。

与JNI相关的文件

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>

#ifndef _Included_Mp3Encoder
#define _Included_Mp3Encoder
#ifdef __cplusplus
extern "C" {
#endif

JNIEXPORT void JNICALL Java_com_dorachat_dorachat_recorder_mp3_Mp3Encoder_init
  (JNIEnv *, jclass, jint, jint, jint, jint, jint);

JNIEXPORT jint JNICALL Java_com_dorachat_dorachat_recorder_mp3_Mp3Encoder_encode
  (JNIEnv *, jclass, jshortArray, jshortArray, jint, jbyteArray);

JNIEXPORT jint JNICALL Java_com_dorachat_dorachat_recorder_mp3_Mp3Encoder_flush
  (JNIEnv *, jclass, jbyteArray);

JNIEXPORT void JNICALL Java_com_dorachat_dorachat_recorder_mp3_Mp3Encoder_close
  (JNIEnv *, jclass);

#ifdef __cplusplus
}
#endif
#endif

lame的jni层主要定义4个方法init、encode、flush和close。

#include "lame-3.100_libmp3lame/lame.h"
#include "Mp3Encoder.h"

static lame_global_flags *glf = NULL;

JNIEXPORT void JNICALL Java_com_dorachat_dorachat_recorder_mp3_Mp3Encoder_init(
        JNIEnv *env, jclass cls, jint inSamplerate, jint outChannel,
        jint outSamplerate, jint outBitrate, jint quality) {
    if (glf != NULL) {
        lame_close(glf);
        glf = NULL;
    }
    glf = lame_init();
    lame_set_in_samplerate(glf, inSamplerate);
    lame_set_num_channels(glf, outChannel);
    lame_set_out_samplerate(glf, outSamplerate);
    lame_set_brate(glf, outBitrate);
    lame_set_quality(glf, quality);
    lame_init_params(glf);
}

JNIEXPORT jint JNICALL
Java_com_dorachat_dorachat_recorder_mp3_Mp3Encoder_encode(
        JNIEnv *env, jclass cls, jshortArray buffer_l, jshortArray buffer_r,
        jint samples, jbyteArray mp3buf) {
    jshort* j_buffer_l = (*env)->GetShortArrayElements(env, buffer_l, NULL);

    jshort* j_buffer_r = (*env)->GetShortArrayElements(env, buffer_r, NULL);

    const jsize mp3buf_size = (*env)->GetArrayLength(env, mp3buf);
    jbyte* j_mp3buf = (*env)->GetByteArrayElements(env, mp3buf, NULL);

    int result = lame_encode_buffer(glf, j_buffer_l, j_buffer_r,
            samples, j_mp3buf, mp3buf_size);

    (*env)->ReleaseShortArrayElements(env, buffer_l, j_buffer_l, 0);
    (*env)->ReleaseShortArrayElements(env, buffer_r, j_buffer_r, 0);
    (*env)->ReleaseByteArrayElements(env, mp3buf, j_mp3buf, 0);

    return result;
}

JNIEXPORT jint JNICALL
Java_com_dorachat_dorachat_recorder_mp3_Mp3Encoder_flush(
        JNIEnv *env, jclass cls, jbyteArray mp3buf) {
    const jsize mp3buf_size = (*env)->GetArrayLength(env, mp3buf);
    jbyte* j_mp3buf = (*env)->GetByteArrayElements(env, mp3buf, NULL);

    int result = lame_encode_flush(glf, j_mp3buf, mp3buf_size);

    (*env)->ReleaseByteArrayElements(env, mp3buf, j_mp3buf, 0);

    return result;
}

JNIEXPORT void JNICALL Java_com_dorachat_dorachat_recorder_mp3_Mp3Encoder_close(
        JNIEnv *env, jclass cls) {
    lame_close(glf);
    glf = NULL;
}

这个需要你会一点C语言的基础,然后就可以轻松调用lame库的函数了。

package com.dorachat.dorachat.recorder.mp3;

public class Mp3Encoder {

    static {
        System.loadLibrary("mp3lame");
    }

    public native static void close();

    public native static int encode(short[] buffer_l, short[] buffer_r, int samples, byte[] mp3buf);

    public native static int flush(byte[] mp3buf);

    public native static void init(int inSampleRate, int outChannel, int outSampleRate, int outBitrate, int quality);

    public static void init(int inSampleRate, int outChannel, int outSampleRate, int outBitrate) {
        init(inSampleRate, outChannel, outSampleRate, outBitrate, 7);
    }
}

我们再看一下java层。

Java层大致实现

我们录音肯定是要在后台Service中进行的,可以使用onStartCommand进行调用,这里只是一个简单的功能,就不使用aidl跨进程了。开始录音时我们需要开一个线程去采样音频数据。

public void start(String filePath, RecordConfig config) {
    this.currentConfig = config;
    if (state != RecordState.IDLE && state != RecordState.STOP) {
        Logger.e(TAG, "状态异常当前状态: %s", state.name());
        return;
    }
    resultFile = new File(filePath);
    String tempFilePath = getTempFilePath();

    LogUtils.dformat(TAG, "----------------开始录制 %s------------------------", currentConfig.getFormat().name());
    LogUtils.dformat(TAG, "参数: %s", currentConfig.toString());
    LogUtils.iformat(TAG, "pcm缓存 tmpFile: %s", tempFilePath);
    LogUtils.iformat(TAG, "录音文件 resultFile: %s", filePath);


    tmpFile = new File(tempFilePath);
    audioRecordThread = new AudioRecordThread();
    audioRecordThread.start();
}

private class AudioRecordThread extends Thread {
    private AudioRecord audioRecord;
    private int bufferSize;

    AudioRecordThread() {
        bufferSize = AudioRecord.getMinBufferSize(currentConfig.getSampleRate(),
                currentConfig.getChannelConfig(), currentConfig.getEncodingConfig()) * RECORD_AUDIO_BUFFER_TIMES;
        Logger.d(TAG, "record buffer size = %s", bufferSize);
        audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, currentConfig.getSampleRate(),
                currentConfig.getChannelConfig(), currentConfig.getEncodingConfig(), bufferSize);
        if (currentConfig.getFormat() == RecordConfig.RecordFormat.MP3) {
            if (mp3EncodeThread == null) {
                initMp3EncoderThread(bufferSize);
            } else {
                LogUtils.e("mp3EncodeThread != null, 请检查代码");
            }
        }
    }

    @Override
    public void run() {
        super.run();
        switch (currentConfig.getFormat()) {
            case MP3:
                startMp3Recorder();
                break;
            default:
                startPcmRecorder();
                break;
        }
    }

    private void startPcmRecorder() {
        state = RecordState.RECORDING;
        notifyState();
        LogUtils.d("开始录制PCM");
        FileOutputStream fos = null;
        try {
            fos = new FileOutputStream(tmpFile);
            audioRecord.startRecording();
            byte[] byteBuffer = new byte[bufferSize];

            while (state == RecordState.RECORDING) {
                int end = audioRecord.read(byteBuffer, 0, byteBuffer.length);
                notifyData(byteBuffer);
                fos.write(byteBuffer, 0, end);
                fos.flush();
            }
            audioRecord.stop();
            files.add(tmpFile);
            if (state == RecordState.STOP) {
                makeFile();
            } else {
                LogUtils.i("暂停");
            }
        } catch (Exception e) {
            LogUtils.e(e.getMessage());
            notifyError("录音失败");
        } finally {
            try {
                if (fos != null) {
                    fos.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        if (state != RecordState.PAUSE) {
            state = RecordState.IDLE;
            notifyState();
            LogUtils.d("录音结束");
        }
    }

    private void startMp3Recorder() {
        state = RecordState.RECORDING;
        notifyState();

        try {
            audioRecord.startRecording();
            short[] byteBuffer = new short[bufferSize];

            while (state == RecordState.RECORDING) {
                int end = audioRecord.read(byteBuffer, 0, byteBuffer.length);
                if (mp3EncodeThread != null) {
                    mp3EncodeThread.addChangeBuffer(new Mp3EncodeThread.ChangeBuffer(byteBuffer, end));
                }
                notifyData(ByteUtils.toBytes(byteBuffer));
            }
            audioRecord.stop();
        } catch (Exception e) {
            LogUtils.e(e.getMessage());
            notifyError("录音失败");
        }
        if (state != RecordState.PAUSE) {
            state = RecordState.IDLE;
            notifyState();
            stopMp3Encoded();
        } else {
            LogUtils.d("暂停");
        }
    }
}

private void stopMp3Encoded() {
    if (mp3EncodeThread != null) {
        mp3EncodeThread.stopSafe(new Mp3EncodeThread.EncordFinishListener() {
            @Override
            public void onFinish() {
                notifyFinish();
                mp3EncodeThread = null;
            }
        });
    } else {
        LogUtils.e("mp3EncodeThread is null, 代码业务流程有误,请检查!");
    }
}

private void makeFile() {
    switch (currentConfig.getFormat()) {
        case MP3:
            return;
        case WAV:
            mergePcmFile();
            makeWav();
            break;
        case PCM:
            mergePcmFile();
            break;
        default:
            break;
    }
    notifyFinish();
    LogUtils.i("录音完成! path: %s ; 大小:%s", resultFile.getAbsoluteFile(), resultFile.length());
}

/**
 * 添加Wav头文件。
 */
private void makeWav() {
    if (!FileUtils.isFile(resultFile) || resultFile.length() == 0) {
        return;
    }
    byte[] header = WavUtils.generateWavFileHeader((int) resultFile.length(), currentConfig.getSampleRate(), currentConfig.getChannelCount(), currentConfig.getEncoding());
    WavUtils.writeHeader(resultFile, header);
}

/**
 * 合并文件。
 */
private void mergePcmFile() {
    boolean mergeSuccess = mergePcmFiles(resultFile, files);
    if (!mergeSuccess) {
        notifyError("合并失败");
    }
}

/**
 * 合并PCM文件。
 *
 * @param recordFile 输出文件
 * @param files      多个文件源
 * @return 是否成功
 */
private boolean mergePcmFiles(File recordFile, List<File> files) {
    if (recordFile == null || files == null || files.size() <= 0) {
        return false;
    }

    FileOutputStream fos = null;
    BufferedOutputStream outputStream = null;
    byte[] buffer = new byte[1024];
    try {
        fos = new FileOutputStream(recordFile);
        outputStream = new BufferedOutputStream(fos);

        for (int i = 0; i < files.size(); i++) {
            BufferedInputStream inputStream = new BufferedInputStream(new FileInputStream(files.get(i)));
            int readCount;
            while ((readCount = inputStream.read(buffer)) > 0) {
                outputStream.write(buffer, 0, readCount);
            }
            inputStream.close();
        }
    } catch (Exception e) {
        LogUtils.e(e.getMessage());
        return false;
    } finally {
        try {
            if (outputStream != null) {
                outputStream.close();
            }
            if (fos != null) {
                fos.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    for (int i = 0; i < files.size(); i++) {
        files.get(i).delete();
    }
    files.clear();
    return true;
}

/**
 * 根据当前的时间生成相应的文件名。
 */
private String getTempFilePath() {
    String fileDir = String.format(Locale.getDefault(), "%s/Record/", Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC).getAbsolutePath());
    if (!FileUtils.createOrExistsDir(fileDir)) {
        LogUtils.e("文件夹创建失败:" + fileDir);
    }
    String fileName = String.format(Locale.getDefault(), "record_tmp_%s", FileUtils.getNowString(new SimpleDateFormat("yyyyMMdd_HH_mm_ss", Locale.SIMPLIFIED_CHINESE)));
    return String.format(Locale.getDefault(), "%s%s.pcm", fileDir, fileName);
}

/**
 * 表示当前录制状态。
 */
public enum RecordState {
    /**
     * 空闲状态。
     */
    IDLE,
    /**
     * 录音中。
     */
    RECORDING,
    /**
     * 暂停中。
     */
    PAUSE,
    /**
     * 正在停止。
     */
    STOP,
    /**
     * 录音流程结束(转换结束)。
     */
    FINISH
}

在Mp3EncodeThread中init初始化录制参数,然后调用encode进行编码录制,录制完成调用flush把缓冲区冲一下水,清洗一下。最后调用一下close,完美。
最后附上录制流程细节的代码。

package com.dorachat.dorachat.recorder.mp3;

import com.dorachat.dorachat.recorder.RecordConfig;
import com.dorachat.dorachat.recorder.RecordService;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;

public class Mp3EncodeThread extends Thread {

    private static final String TAG = Mp3EncodeThread.class.getSimpleName();
    private List<ChangeBuffer> cacheBufferList = Collections.synchronizedList(new LinkedList<ChangeBuffer>());
    private File file;
    private FileOutputStream os;
    private byte[] mp3Buffer;
    private EncodeFinishListener encodeFinishListener;

    /**
     * 是否已停止录音。
     */
    private volatile boolean isOver = false;

    /**
     * 是否继续轮询数据队列。
     */
    private volatile boolean start = true;

    public Mp3EncodeThread(File file, int bufferSize) {
        this.file = file;
        mp3Buffer = new byte[(int) (7200 + (bufferSize * 2 * 1.25))];
        RecordConfig currentConfig = RecordService.getCurrentConfig();
        int sampleRate = currentConfig.getSampleRate();

        Mp3Encoder.init(sampleRate, currentConfig.getChannelCount(), sampleRate, currentConfig.getRealEncoding());
    }

    @Override
    public void run() {
        try {
            this.os = new FileOutputStream(file);
        } catch (FileNotFoundException e) {
            return;
        }

        while (start) {
            ChangeBuffer next = next();
            lameData(next);
        }
    }

    public void addChangeBuffer(ChangeBuffer changeBuffer) {
        if (changeBuffer != null) {
            cacheBufferList.add(changeBuffer);
            synchronized (this) {
                notify();
            }
        }
    }

    public void stopSafe(EncodeFinishListener encodeFinishListener) {
        this.encodeFinishListener = encodeFinishListener;
        isOver = true;
        synchronized (this) {
            notify();
        }
    }

    private ChangeBuffer next() {
        for (;;) {
            if (cacheBufferList == null || cacheBufferList.size() == 0) {
                try {
                    if (isOver) {
                        finish();
                    }
                    synchronized (this) {
                        wait();
                    }
                } catch (Exception e) {
                }
            } else {
                return cacheBufferList.remove(0);
            }
        }
    }

    private void lameData(ChangeBuffer changeBuffer) {
        if (changeBuffer == null) {
            return;
        }
        short[] buffer = changeBuffer.getData();
        int readSize = changeBuffer.getReadSize();
        if (readSize > 0) {
            int encodedSize = Mp3Encoder.encode(buffer, buffer, readSize, mp3Buffer);
            try {
                os.write(mp3Buffer, 0, encodedSize);
            } catch (IOException e) {
            }
        }
    }

    private void finish() {
        start = false;
        final int flushResult = Mp3Encoder.flush(mp3Buffer);
        if (flushResult > 0) {
            try {
                os.write(mp3Buffer, 0, flushResult);
                os.close();
            } catch (final IOException e) {
            }
        }
        if (encodeFinishListener != null) {
            encodeFinishListener.onFinish();
        }
    }

    public static class ChangeBuffer {
        private short[] rawData;
        private int readSize;

        public ChangeBuffer(short[] rawData, int readSize) {
            this.rawData = rawData.clone();
            this.readSize = readSize;
        }

        short[] getData() {
            return rawData;
        }

        int getReadSize() {
            return readSize;
        }
    }

    public interface EncodeFinishListener {
    
        /**
         * 格式转换完毕。
         */
        void onFinish();
    }
}

总结

这只是Android音视频开发的冰山一角,主要是为了演示大概的开发流程,如果需要深入研究Android NDK,一定需要先把C/C++语言学好,这样才能走得更远。

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

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

相关文章

BUG: VS Code C++输出中文乱码

BUG: VS Code C输出中文乱码 环境 Windows 11 VS Code 编辑器详情 在Windows 使用 cout 函数输出中文时出现乱码 问题的原因在cmd的显示编码和c程序编码的不同。cmd默认的是gbk编码&#xff0c;而VS Code 软件的CMD终端默认是utf-8编码&#xff0c;因而在输出中文文本时会出…

QQ名片满级会员展示生成HTML源码

源码介绍 QQ名片满级会员展示生成HTML源码&#xff0c;源码由HTMLCSSJS组成&#xff0c;双击html文件可以本地运行效果&#xff0c;也可以上传到服务器里面&#xff0c;保存素材去选择QQ个性名片-选择大图模板-把图上传照片墙即可 源码效果 源码下载 蓝奏云&#xff1a;http…

唯众云课堂:领航智慧教育,赋能职教未来,打造高效人才培养新平台

随着《中国智慧教育发展报告 2023》的发布&#xff0c;智慧教育被正式定义为数字教育发展的高级阶段。然而&#xff0c;各职院在智慧教育的发展道路上&#xff0c;往往面临着诸多挑战&#xff0c;如缺乏一体化教学平台、优质教学资源不足等。唯众凭借深厚的产业洞察与教育实践经…

Pytorch中的torch.save()文件保存格式探索以及mmdetection加载预训练模型参数对不齐和收到意外参数报错解决方案

使用mmdetection时遇到的问题比较多&#xff0c;首先要对自己要使用的预训练模型有一定的了解&#xff0c;并且懂得使用各种分类模型时不同的模型不同任务执行阶段需要参数上的对其。&#xff08;比如mask-rcnn和它的三个头之间的参数&#xff09;。 首先&#xff0c;谈谈torc…

基于SpringBoot设计模式之结构型设计模式·适配器模式

文章目录 介绍开始使用委托的适配器&#xff08;媒体播放器&#xff09;架构图定义被适配者定义需求接口定义适配者 使用继承的适配器&#xff08;手机充电接口&#xff09;架构图定义被适配者定义需求接口定义适配者 测试样例 总结优点缺点 介绍 在程序世界中&#xff0c;经常…

Python | Leetcode Python题解之第115题不同的子序列

题目&#xff1a; 题解&#xff1a; class Solution:def numDistinct(self, s: str, t: str) -> int:m, n len(s), len(t)if m < n:return 0dp [[0] * (n 1) for _ in range(m 1)]for i in range(m 1):dp[i][n] 1for i in range(m - 1, -1, -1):for j in range(n …

词法与语法分析器介绍

概述 词法和语法可以使用正则表达式和BNF范式表达&#xff0c;而最终描述文法含义的事状态转换图 Lex与YACC 词法分析器Lex 词法分析词Lex&#xff0c;是一种生成词法分析的工具&#xff0c;描述器是识别文本中词汇模式的程序&#xff0c;这些词汇模式是在特殊的句子结构中…

再见PS,Canva Create正式上线

再见&#xff0c;Photoshop&#xff01; Canva Create 正式上线&#xff0c;太疯狂了&#xff01;&#xff01; Canva是一款著名的免费在线AI图像生成器 构想你的创意&#xff0c;然后将其添加到你的设计中。使用最佳的AI图像生成器&#xff0c;观察你的文字和短语变换成美丽…

探索Java的DNA-JVM字节码深度解析

引言 在Java的世界里&#xff0c;JVM&#xff08;Java虚拟机&#xff09;是我们程序运行的心脏。而字节码&#xff0c;作为JVM的血液&#xff0c;携带着程序的执行指令。今天&#xff0c;我们将深入探索Java字节码的奥秘&#xff0c;一窥JVM如何将人类可读的代码转化为机器可执…

【香橙派AIpro】开箱测评

1.板子开箱 哟&#xff0c;看起来还不错哦&#xff01;&#xff01;&#xff01; 收货清单&#xff1a; 主板*1 1.5m数据线*1 充电头*1 1.1.充电头 近65W的充电头&#xff0c;不错不错。 1.2.主板 1.2.1.上面 哇噢&#xff0c;还送了2.4/5G的WiFi和蓝牙天线。 emm&#xf…

【NumPy】关于numpy.argsort()函数,看这一篇文章就够了

&#x1f9d1; 博主简介&#xff1a;阿里巴巴嵌入式技术专家&#xff0c;深耕嵌入式人工智能领域&#xff0c;具备多年的嵌入式硬件产品研发管理经验。 &#x1f4d2; 博客介绍&#xff1a;分享嵌入式开发领域的相关知识、经验、思考和感悟&#xff0c;欢迎关注。提供嵌入式方向…

计算机专业必考之计算机指令设计格式

计算机指令设计格式 例题&#xff1a; 1.设相对寻址的转移指令占3个字节&#xff0c;第一字节为操作码&#xff0c;第二&#xff0c;第三字节为相对偏移量&#xff0c; 数据在存储器以低地址为字地址的存放方式。 每当CPU从存储器取出一个字节时候&#xff0c;自动完成&…

通过Zerossl给IP申请免费SSL证书, 实现https ip访问

参考通过Zerossl给IP申请免费SSL证书 | LogDicthttps://www.logdict.com/archives/tong-guo-zerosslgei-ipshen-qing-mian-fei-sslzheng-shu

【Linux-LCD 驱动】

Linux-LCD 驱动 ■ Framebuffer 简称 fb■ LCD 驱动程序编写■ 1、LCD 屏幕 IO 配置■ 2、LCD 屏幕参数节点信息修改■ 3、LCD 屏幕背光节点信息■ 4、使能 Linux logo 显示 ■ 设置 LCD 作为终端控制台■ 1、设置 uboot 中的 bootargs■ 2、修改/etc/inittab 文件 ■ LCD 背光…

亚马逊高效广告打法及数据优化,亚马逊高阶广告打法课

课程下载&#xff1a;https://download.csdn.net/download/m0_66047725/89342733 更多资源下载&#xff1a;关注我。 课程内容&#xff1a; 001.1-亚马逊的广告漏斗和A9算法的升级变化.mp4 002.2-流量入口解析和广告的曝光机制.mp4 003.3-标签理论 .mp4 004.4-不同广告类…

小程序内使用路由

一:使用组件 1)创建组件 2)在需要的页面的json/app.json可实现局部使用和全局使用 在局部的话,对象内第一层,window配置也是第一层,而在全局配置也是在第一层,window在window对象内.第二层.内部执行遍历不一样. 3)页面使用 上述所写可实现在页面内使用组件.效果是页面内可以将…

预热 618,编程好书推荐——提升你的代码力

文章目录 &#x1f4cb;前言&#x1f3af;编程好书推荐&#x1f4d8; Java领域的经典之作&#x1f40d; Python学习者的宝典&#x1f310; 前端开发者的权威指南&#x1f512; 并发编程的艺术&#x1f916; JVM的深入理解&#x1f3d7; 构建自己的编程语言&#x1f9e0; 编程智…

SolidWorks教育版 学生使用的优势

在工程技术领域的学习中&#xff0c;计算机辅助设计软件&#xff08;CAD&#xff09;如SolidWorks已经成为学生掌握专业知识和技能的必要工具。SolidWorks教育版作为专为教育机构和学生设计的版本&#xff0c;不仅提供了与商业版相同的强大功能&#xff0c;还为学生带来了诸多独…

传输层安全性 (TLS)

传输层安全 (TLS) 旨在提供传输层的安全性。TLS 源自称为安全套接字层 (SSL)的安全协议。 TLS 确保任何第三方都无法窃听或篡改任何消息。 TLS 有几个好处&#xff1a; ● 加密&#xff1a; TLS/SSL 可以帮助使用加密来保护传输的数据。 ● 互操作性&#xff1a; TLS/S…

利用audacity和ffmpeg制作测试音频文件

最近要用SIPP测试一个场景&#xff0c;需要发送双声道/16K采样率/16bit量化的PCM流&#xff0c;但是下载的素材往往不能满足参数要求。那么就自己制作。 首先下载mp3文件&#xff0c;并用audacity打开。 接下来&#xff0c;点击菜单栏中轨道-重采样&#xff0c;将采样频率设为1…