讯飞与腾讯云:Android 语音识别服务对比选择

目录

一、讯飞语音识别

[1.1 讯飞语音识别介绍](#1.1 讯飞语音识别介绍)

[1.1.1 功能特点](#1.1.1 功能特点)

[1.1.2 优势](#1.1.2 优势)

[1.2 接入流程](#1.2 接入流程)

[1.2.1 注册账号并创建应用](#1.2.1 注册账号并创建应用)

[1.2.2 下载SDK等相关资料](#1.2.2 下载SDK等相关资料)

[1.2.3 导入SDK](#1.2.3 导入SDK)

[1.2.4 添加用户权限](#1.2.4 添加用户权限)

[1.2.5 初始化讯飞SDK](#1.2.5 初始化讯飞SDK)

[1.2.6 初始化语音识别对象](#1.2.6 初始化语音识别对象)

[1.2.7 显示结果](#1.2.7 显示结果)

二、腾讯云语音识别

[2.1 腾讯云语音识别介绍](#2.1 腾讯云语音识别介绍)

[2.1.1 功能特点](#2.1.1 功能特点)

[2.1.2 优势](#2.1.2 优势)

[2.2 接入流程](#2.2 接入流程)

[2.2.1 注册腾讯云账号](#2.2.1 注册腾讯云账号)

[2.2.2 获取相关的凭证信息](#2.2.2 获取相关的凭证信息)

[2.2.3 下载SDK等相关资料](#2.2.3 下载SDK等相关资料)

[2.2.4 导入SDK和添加其他依赖](#2.2.4 导入SDK和添加其他依赖)

[2.2.5 添加用户权限](#2.2.5 添加用户权限)

[2.2.6 初始化腾讯云SDK](#2.2.6 初始化腾讯云SDK)

[2.2.7 设置识别结果回调](#2.2.7 设置识别结果回调)

[2.2.8 录音文件直接识别](#2.2.8 录音文件直接识别)

[2.2.9 录音并识别语音](#2.2.9 录音并识别语音)

[2.2.10 recognize 介绍](#2.2.10 recognize 介绍)

三、选择建议

相关推荐


在 移动端 接入语音识别方面,讯飞和腾讯云都是优秀的选择,但各有其特点和优势。以下是对两者的详细比较:

一、讯飞语音识别

1.1 讯飞语音识别介绍

1.1.1 功能特点

1.提供全面的语音识别功能,包括实时语音识别和离线语音识别。

2.支持多种语言识别,满足不同语种用户的需求。(普通话/英语免费,其他语音可试用半年。试用到期后需单独购买,价格为:2万/个/年)

3.提供丰富的SDK和API接口,方便开发者集成和使用。

1.1.2 优势

1.讯飞在语音识别领域有较高的知名度和市场占有率。

2.提供了详细的开发文档和示例代码,方便开发者快速上手。

3.支持定制化开发,可以根据用户需求进行个性化定制。

1.2 接入流程

1.2.1 注册账号并创建应用

注册讯飞开放平台账号,创建应用并获得AppID。

1.2.2 下载SDK等相关资料

直接下载SDK,SDK中包含简易可运行的Demo。

1.2.3 导入SDK

将在官网下载的Android SDK 压缩包中libs目录下所有子文件拷贝至Android工程的libs目录下。

sdk下文件夹main/assets/,自带UI页面(iflytek文件夹)和相关其他服务资源文件(语法文件、音频示例、词表),使用自带UI接口时,可以将assets/iflytek文件拷贝到项目中;我这用到是自己写的界面所以仅导入了libs目录下文件。

1.2.4 添加用户权限

在工程 AndroidManifest.xml 文件中添加如下权限,在实际项目中还需要动态申请权限。

java 复制代码
<!--连接网络权限,用于执行云端语音能力 -->
<uses-permission android:name="android.permission.INTERNET"/>
<!--获取手机录音机使用权限,听写、识别、语义理解需要用到此权限 -->
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<!--读取网络信息状态 -->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<!--获取当前wifi状态 -->
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<!--允许程序改变网络连接状态 -->
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
<!--读取手机信息权限 -->
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
<!--读取联系人权限,上传联系人需要用到此权限 -->
<uses-permission android:name="android.permission.READ_CONTACTS"/>
<!--外存储写权限,构建语法需要用到此权限 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<!--外存储读权限,构建语法需要用到此权限 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<!--配置权限,用来记录应用配置信息 -->
<uses-permission android:name="android.permission.WRITE_SETTINGS"/>
<!--手机定位信息,用来为语义等功能提供定位,提供更精准的服务-->
<!--定位信息是敏感信息,可通过Setting.setLocationEnable(false)关闭定位请求 -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<!--如需使用人脸识别,还要添加:摄像头权限,拍照需要用到 -->
<uses-permission android:name="android.permission.CAMERA" />

注意:如需在打包或者生成APK的时候进行混淆,请在proguard.cfg中添加如下代码:

Groovy 复制代码
-keep class com.iflytek.**{*;}
-keepattributes Signature

1.2.5 初始化讯飞SDK

初始化即创建语音配置对象,只有初始化后才可以使用MSC的各项服务。建议将初始化放在程序入口处(如Application、Activity的onCreate方法),初始化代码如下:

java 复制代码
// 将"12345678"替换成您申请的APPID,申请地址:http://www.xfyun.cn
// 请勿在"="与appid之间添加任何空字符或者转义符
SpeechUtility.createUtility(context, SpeechConstant.APPID +"=12345678");

// public class SpeechConstant {
//     public static final java.lang.String APPID = "appid";
//     ......
// }

1.2.6 初始化语音识别对象

java 复制代码
    private void initSpeech() {
        // 使用SpeechRecognizer对象,可根据回调消息自定义界面;
        mIat = SpeechRecognizer.createRecognizer(this, mInitListener);
        setParam();
    }

    /**
     * 初始化监听器。
     */
    private InitListener mInitListener = code -> {
        Log.d(TAG, "SpeechRecognizer init() code = " + code);
        if (code != ErrorCode.SUCCESS) {
            //showTip("初始化失败,错误码:" + code + ",请点击网址https://www.xfyun.cn/document/error-code查询解决方案");
        }
    };

    /**
     * 参数设置
     *
     * @return
     */
    public void setParam() {
        if (mIat != null) {
            // 清空参数
            mIat.setParameter(SpeechConstant.PARAMS, null);
            // 设置听写引擎,此处engineType为"cloud"
            mIat.setParameter( SpeechConstant.ENGINE_TYPE, engineType );
            //设置返回结果格式,目前支持json,xml以及plain 三种格式,其中plain为纯听写文本内容
            mIat.setParameter(SpeechConstant.RESULT_TYPE, "json");
            // 设置语言(目前普通话,可切换成英文)
            mIat.setParameter(SpeechConstant.LANGUAGE, "zh_cn");
            // 设置语言区域
            mIat.setParameter(SpeechConstant.ACCENT, "mandarin");

            // 设置语音前端点:静音超时时间,即用户多长时间不说话则当做超时处理
            //取值范围{1000~10000}
            mIat.setParameter(SpeechConstant.VAD_BOS, "10000");

            // 设置语音后端点:后端点静音检测时间,即用户停止说话多长时间内即认为不再输入, 自动停止录音
            //取值范围{1000~10000}
            mIat.setParameter(SpeechConstant.VAD_EOS, "1000");

            // 设置标点符号,设置为"0"返回结果无标点,设置为"1"返回结果有标点
            mIat.setParameter(SpeechConstant.ASR_PTT, "0");

            // 设置音频保存路径,保存音频格式支持pcm、wav.
            mIat.setParameter(SpeechConstant.AUDIO_FORMAT, "wav");
            mIat.setParameter(SpeechConstant.ASR_AUDIO_PATH,
                    getExternalFilesDir("msc").getAbsolutePath() + "/iat.wav");
        }
    }

1.2.7 开始录音

java 复制代码
    public void startListen() {
        buffer.setLength(0);
        mIatResults.clear();
        int ret = mIat.startListening(mRecognizerListener);
        if (ret != ErrorCode.SUCCESS) {
            Log.d(TAG, "听写失败,错误码:" + ret + ",请点击网址https://www.xfyun.cn/document/error-code查询解决方案");
        } else {
            Log.d(TAG, "开始说话");
            if (!isSoundRecording){
                isSoundRecording = true;
                runOnUiThread(() -> {
                    binding.llSoundRecording.setVisibility(View.VISIBLE);
                    binding.ivStopSoundRecording.setVisibility(View.VISIBLE);
                    if (animationDrawable != null) {
                        animationDrawable.start();
                    }
                });
            }

        }
    }
    /**
     * 听写监听器。
     */
    private RecognizerListener mRecognizerListener = new RecognizerListener() {

        @Override
        public void onBeginOfSpeech() {
            // 此回调表示:sdk内部录音机已经准备好了,用户可以开始语音输入
            Log.d(TAG, "RecognizerListener.onEvent:sdk内部录音机已经准备好了,用户可以开始语音输入");
        }

        @Override
        public void onError(SpeechError error) {
            // Tips:
            // 错误码:10118(您没有说话),可能是录音机权限被禁,需要提示用户打开应用的录音权限。
            Log.d(TAG, "RecognizerListener.onError " + error.getPlainDescription(true));

        }

        @Override
        public void onEndOfSpeech() {
            // 此回调表示:检测到了语音的尾端点,已经进入识别过程,不再接受语音输入
            Log.d(TAG, "RecognizerListener.onEndOfSpeech ");

        }

        @Override
        public void onResult(RecognizerResult recognizerResult, boolean b) {
            Log.d(TAG, "RecognizerListener.onResult 结束" + recognizerResult.getResultString());
            Log.d(TAG, results.getResultString());
            if (isLast) {
                Log.d(TAG, "onResult 结束");
            }
            //设置返回结果格式,目前支持json,xml以及plain 三种格式,其中plain为纯听写文本内容
            //mIat.setParameter(SpeechConstant.RESULT_TYPE, "json");
            //在初始化的时候我们设置的事json,所以处理json就可以了。
            if (resultType.equals("json")) {
                printResult(results);
                return;
            }
//            if (resultType.equals("plain")) {
//                buffer.append(results.getResultString());
//                mResultText.setText(buffer.toString());
//                mResultText.setSelection(mResultText.length());
//            }
        }


        @Override
        public void onVolumeChanged(int volume, byte[] data) {
            Log.d(TAG, "RecognizerListener.onVolumeChanged");
        }

        @Override
        public void onEvent(int eventType, int arg1, int arg2, Bundle obj) {
            Log.d(TAG, "RecognizerListener.onEvent" + eventType);
        }
    };

1.2.7 显示结果

拿到结果,那后面还不是你说了算

java 复制代码
    private HashMap<String, String> mIatResults = new LinkedHashMap<>(); 
   /**
     * 显示结果
     */
    private void printResult(RecognizerResult results) {
        String text = JsonParser.parseIatResult(results.getResultString());
        String sn = null;
        // 读取json结果中的sn字段
        try {
            JSONObject resultJson = new JSONObject(results.getResultString());
            sn = resultJson.optString("sn");
        } catch (JSONException e) {
            e.printStackTrace();
        }

        mIatResults.put(sn, text);

        StringBuffer resultBuffer = new StringBuffer();
        for (String key : mIatResults.keySet()) {
            resultBuffer.append(mIatResults.get(key));
        }
        mResultText.setText(resultBuffer.toString());
        mResultText.setSelection(mResultText.length());
    }

讯飞错误码:错误码查询 - 讯飞

讯飞官方文档:语音听写 Android SDK 文档 | 讯飞

二、腾讯云语音识别

2.1 腾讯云语音识别介绍

2.1.1 功能特点

腾讯云语音识别(ASR)基于深度学习技术,具备较高的语音识别准确性。

提供实时语音识别和离线语音识别两种类型,满足不同场景需求。

支持多种语种和方言识别,如中文、英文、粤语等。

2.1.2 优势

腾讯云作为国内领先的云服务提供商,拥有强大的技术实力和丰富的应用场景。

提供了丰富的语音识别和语音合成产品,可以满足开发者多样化的需求。

提供了可视化控制台和详尽的API文档,方便开发者进行配置和管理。

2.2 接入流程

2.2.1 注册腾讯云账号

注册腾讯云账号(需要个人实名认证/企业认证),并在控制台中创建语音识别应用。

2.2.2 获取相关的凭证信息

获取相关的凭证信息(如SecretId和SecretKey),用于后续的API调用。

2.2.3 下载SDK等相关资料

直接下载SDK,SDK中包含简易可运行的Demo。

2.2.4 导入SDK和添加其他依赖

添加录音文件识别 SDK aar,将 asr-file-recognize-release.aar 放在 libs 目录下,在 App 的 build.gradle 文件中添加。

Groovy 复制代码
implementation(name: 'asr-file-recognize-release', ext: 'aar')
implementation 'com.google.code.gson:gson:2.8.5'

2.2.5 添加用户权限

在工程 AndroidManifest.xml 文件中添加如下权限,在实际项目中还需要动态申请权限。

Groovy 复制代码
< uses-permission android:name="android.permission.INTERNET"/>
<!--获取手机录音机使用权限,听写、识别、语义理解需要用到此权限 -->
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<!--读取网络信息状态 -->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<!--获取当前wifi状态 -->
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<!--允许程序改变网络连接状态 -->
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
<!--外存储写权限,构建语法需要用到此权限 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<!--外存储读权限,构建语法需要用到此权限 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

注意:如需在打包或者生成APK的时候进行混淆,请在proguard.cfg中添加如下代码:

Groovy 复制代码
-keepclasseswithmembernames class * { # 保持 native 方法不被混淆
native <methods>;
}
-keep public class com.tencent.cloud.qcloudasrsdk.*

2.2.6 初始化腾讯云SDK

java 复制代码
        int appId = xxxxx;
        int projectId = 0; //此参数固定为0;
        String secretId = "xxxxxx";
        String secretKey = "xxx";
        if (fileFlashRecognizer == null) {
                /**直接鉴权**/
            fileFlashRecognizer = new QCloudFlashRecognizer(appId, secretId, secretKey);

               /**使用临时密钥鉴权
                * * 1.通过sts 获取到临时证书 (secretId secretKey  token) ,此步骤应在您的服务器端实现,见https://cloud.tencent.com/document/product/598/33416
                *   2.通过临时密钥调用接口
                * **/
//            fileFlashRecognizer = new QCloudFlashRecognizer(DemoConfig.apppId, "临时secretId", "临时secretKey","对应的token");

        }

2.2.7 设置识别结果回调

java 复制代码
    //设置识别结果回调
    fileFlashRecognizer.setCallback(this);

    public interface QCloudFlashRecognizerListener {
        /**
         * 识别结果回调
         * @param recognizer 录音文件识别实例
         * @param result 服务器返回的识别结果 api文档 https://cloud.tencent.com/document/product/1093/52097
         * @param exception 异常信息
         *
         */
        void recognizeResult(QCloudFlashFileRecognizer recognizer,String result, int status,Exception exception);
    }

    //录音文件识别结果回调 ,详见api文档 https://cloud.tencent.com/document/product/1093/52097
    @Override
    public void recognizeResult(QCloudFlashRecognizer recognizer,  String result, Exception exception) {
        showLoading(false);
        mStartRec.setEnabled(true);
        mRecognize.setEnabled(true);
        Log.i(this.getClass().getName(), result);

        TextView textView = findViewById(R.id.recognize_flash_text_view);
        if (exception != null){
            textView.setText(exception.getLocalizedMessage());
        }else {
            textView.setText(result);
        }
        if (mPcmTmpFile != null){
            mPcmTmpFile.delete();
            mPcmTmpFile = null;
        }

    }

2.2.8 录音文件直接识别

java 复制代码
        InputStream is = null;
        try {
            AssetManager am = getResources().getAssets();
            is = am.open("test2.mp3");
            int length = is.available();
            byte[] audioData = new byte[length];
            is.read(audioData);

            QCloudFlashRecognitionParams params = (QCloudFlashRecognitionParams) QCloudFlashRecognitionParams.defaultRequestParams();

            /**支持传音频文件数据或者音频文件路径,如果同时调用setData和setPath,sdk内将忽略setPath
             *  音频文件支持100M以内的文件,如果使用setData直接传音频文件数据,需要避免数据过大引发OOM,大文件建议传路径
             *  setVoiceFormat必须正确,否则服务器端将无法解析
             *  参数解释详解API文档https://cloud.tencent.com/document/product/1093/52097
             * **/
            params.setData(audioData);
//                    params.setPath("/sdcard/test2.mp3"); //需要读写权限
            params.setVoiceFormat("mp3"); //音频格式。支持 wav、pcm、ogg-opus、speex、silk、mp3、m4a、aac。

            /**以下参数不设置将使用默认值**/
//                    params.setEngineModelType("16k_zh");//引擎模型类型,默认16k_zh。8k_zh:8k 中文普通话通用;16k_zh:16k 中文普通话通用;16k_zh_video:16k 音视频领域。
//                    params.setFilterDirty(0);// 0 :默认状态 不过滤脏话 1:过滤脏话
//                    params.setFilterModal(0);// 0 :默认状态 不过滤语气词  1:过滤部分语气词 2:严格过滤
//                    params.setFilterPunc(0);// 0 :默认状态 不过滤句末的句号 1:滤句末的句号
//                    params.setConvertNumMode(1);//1:默认状态 根据场景智能转换为阿拉伯数字;0:全部转为中文数字。
//                    params.setSpeakerDiarization(0); //是否开启说话人分离(目前支持中文普通话引擎),默认为0,0:不开启,1:开启。
//                    params.setFirstChannelOnly(1); //是否只识别首个声道,默认为1。0:识别所有声道;1:识别首个声道。
//                    params.setWordInfo(0); //是否显示词级别时间戳,默认为0。0:不显示;1:显示,不包含标点时间戳,2:显示,包含标点时间戳。

            /**网络超时时间。
             * 注意:如果设置过短的时间,网络超时断开将无法获取到识别结果;
             * 如果网络断开前音频文件已经上传完成,将会消耗该音频时长的识别额度
             * **/
//                    params.setConnectTimeout(30 * 1000);//单位:毫秒,默认30秒
//                    params.setReadTimeout(600 * 1000);//单位:毫秒,默认10分钟
//                    params.setReinforceHotword(1); // 开启热词增强
//                    params.setSentenceMaxLength(10);

            long ret = 0;
            ret = fileFlashRecognizer.recognize(params);

            if (ret >= 0) {
                showLoading(true);
                mStartRec.setEnabled(false);
                mRecognize.setEnabled(false);
            }
        } catch (IOException e) {
            showLoading(false);
            onMessage("录音文件不存在");
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (is != null) is.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

2.2.9 录音并识别语音

java 复制代码
        if (mRecord == null) {
            mRecord = new PcmAudioRecord(); //调用系统录音器录音,调用前请先申请权限
        }
        try {
            mPcmTmpFile = File.createTempFile("pcm_temp", ".pcm");
            boolean ret = mRecord.start(mPcmTmpFile);
            if (ret == false) {
                onMessage("录音器启动失败,请检查权限");
                return;
            }
            isRecording = true;
            showLoading(true);
            mRecognize.setEnabled(false);
            mStartRec.setText("stopRecord");
        } catch (IOException e) {
            e.printStackTrace();
            mStartRec.setEnabled(true);
            mRecognize.setEnabled(true);
            return;
        }
    } else

    {
        if (mRecord == null) {
            mStartRec.setEnabled(true);
            mRecognize.setEnabled(true);
            return;
        }
        mRecord.stop();
        QCloudFlashRecognitionParams params = (QCloudFlashRecognitionParams) QCloudFlashRecognitionParams.defaultRequestParams();
        params.setPath(mPcmTmpFile.getPath()); //需要读写权限
        params.setVoiceFormat("pcm");
        params.setReinforceHotword(1); // 开启热词增强

        try {
            long ret = fileFlashRecognizer.recognize(params);
            if (ret >= 0) {
                showLoading(true);
                mStartRec.setEnabled(false);
                mRecognize.setEnabled(false);
                isRecording = false;
                Button btn = findViewById(R.id.recognize_start_record);
                btn.setText("startRecord");
                showLoading(true);
                mStartRec.setEnabled(false);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

2.2.10 recognize 介绍

java 复制代码
public class QCloudFlashRecognizer extends com.tencent.cloud.qcloudasrsdk.filerecognize.QCloudBaseRecognizer implements com.tencent.cloud.qcloudasrsdk.filerecognize.network.QCloudFlashRecognizeTaskListener {
    /**
     * 初始化方法
     *
     * @param activity  app activity
     * @param appId     腾讯云 appid
     * @param secretId  腾讯云 secretId
     * @param secretKey 腾讯云 secretKey
     */
    public QCloudFlashRecognizer(String appId, String secretId, String secretKey);

    /* * 通过 url 或语音数据调用录音文件识别
     * @param params 请求参数
     * @return 返回本次请求的唯一标识别 requestId
     */
    public long recognize(QCloudFlashRecognitionParams params) throws Exception;
}

在Android项目中调用腾讯云的语音识别API,并处理识别结果。

腾讯云语音识别:录音文件识别极速版-腾讯云

三、选择建议

讯飞语音识别免费额度: 每天500条识别。

腾讯云免费额度:一句话语音识别(每月5000条)、录音文件识别(每月10小时)。

综上所述,讯飞和腾讯云都是优秀的Android语音识别解决方案。开发者在选择时,应根据自身需求、成本因素和用户评价进行综合考虑。

相关推荐

Android SDK 遇到的坑之 AIUI(星火大模型)-CSDN博客文章浏览阅读3.4k次,点赞92次,收藏66次。需要给桌面机器人(医康养)应用做语音指引/控制/健康咨询等功能。AIUI常见错误:唤醒无效;错误码:600103;错误码:600022。_aiui android sdk 配置唤醒词https://shuaici.blog.csdn.net/article/details/141430041Android SDK 遇到的坑之讯飞语音合成-CSDN博客文章浏览阅读1.9k次,点赞50次,收藏36次。loadLibrary msc error:java.lang.UnsatisfiedLinkError: dlopen failed: library "libmsc.so" not found组件未安装.(错误码:21002)_组件未安装.(错误码:21002)https://shuaici.blog.csdn.net/article/details/141169429

相关推荐
开发者每周简报12 分钟前
求职市场变化
人工智能·面试·职场和发展
AI前沿技术追踪25 分钟前
OpenAI 12天发布会:AI革命的里程碑@附35页PDF文件下载
人工智能
余~~1853816280031 分钟前
稳定的碰一碰发视频、碰一碰矩阵源码技术开发,支持OEM
开发语言·人工智能·python·音视频
galileo20161 小时前
LLM与金融
人工智能
DREAM依旧1 小时前
隐马尔科夫模型|前向算法|Viterbi 算法
人工智能
ROBOT玲玉1 小时前
Milvus 中,FieldSchema 的 dim 参数和索引参数中的 “nlist“ 的区别
python·机器学习·numpy
GocNeverGiveUp2 小时前
机器学习2-NumPy
人工智能·机器学习·numpy
浊酒南街2 小时前
决策树(理论知识1)
算法·决策树·机器学习
B站计算机毕业设计超人2 小时前
计算机毕业设计PySpark+Hadoop中国城市交通分析与预测 Python交通预测 Python交通可视化 客流量预测 交通大数据 机器学习 深度学习
大数据·人工智能·爬虫·python·机器学习·课程设计·数据可视化
学术头条3 小时前
清华、智谱团队:探索 RLHF 的 scaling laws
人工智能·深度学习·算法·机器学习·语言模型·计算语言学