初識 Speex 語音壓縮

初識 Speex 語音壓縮

Speex 簡介

Speex 是一個聲音編碼格式,目標是用於網絡電話、在線廣播使用的語音編碼,基於 CELP(一種語音編碼算法)開發,Speex 宣稱可以免費使用,以 BSD 授權條款開放源代碼。

Speex 的開發者將這個格式視爲 Vorbis(通用音頻壓縮格式)的補充。

Speex 是一種有損格式,這意味着使用此格式的音頻,質量將會永久性地降低以減少文件的大小。

開發 Speex 的 Xiph.org 基金會已經宣佈廢棄 Speex,建議改用 Opus 取代。

Speex 是針對網際協議通話技術(VoIP)和基於文件的壓縮。Speex 的設計目標是開發一個保有高質量語音的同時降低其比特率(bit rate)的編碼器。 爲了實現前述目標,Speex 編碼器使用多位比特率(multiple bit rates),並支持超寬頻 (32 kHz 採樣率),寬帶 (16 kHz 採樣率) 和窄帶 (電話通話質量,8 kHz 採樣率)。 由於 Speex 是設計用於 VoIP 而不是手機,因此 Speex 編碼器必須能容忍丟失數據包(lost packets),但不能數據包是損壞的。基於上述的要求,選擇 CELP 算法作爲 Speex 的編碼技術。使用 CELP 的主要原因之一是,CELP 早已證明,它可以同時做到低比特率和高比特率。

Speex 的主要特性歸納如下:

  • 自由軟件 / 開源,無專利保護且使用無需版稅。
  • 集窄帶和寬帶在同一比特流(bit-stream)。
  • 比特率可選擇的範圍很廣 (從 2 kbit/s 至 44 kbit/s)。
  • 動態交換的比特率和可變比特率 (VBR, variable bit-rate)。
  • 語音動態的檢測 (VAD,與 VBR 集成)(1.2 版開始廢棄)。

Speex 性能

目前只測了部分壓縮的性能(設備:一加3T【高通驍龍821】)

Speex 性能(一加3T)

Speex 與 Opus

Opus 是一個有損聲音編碼的格式,由 Xiph.Org 基金會開發,之後由互聯網工程任務組進行標準化,目標是希望用單一格式包含聲音和語音,取代 Speex 和 Vorbis,且適用於網絡上低延遲的即時聲音傳輸,標準格式定義於 RFC 6716 文件。Opus 格式是一個 開放格式,使用上沒有任何專利或限制。

Opus 集成了兩種聲音編碼的技術:以語音編碼爲導向的 SILK 和低延遲的 CELT。Opus 可以無縫調節高低 比特率。在編碼器內部它在較低比特率時使用線性預測編碼在高比特率時候使用變換編碼(在高低比特率交界處也使用兩者結合的編碼方式)。Opus 具有非常低的算法延遲(默認爲 22.5 ms)[3],非常適合用於低延遲語音通話的編碼,像是網絡上的即時聲音流、即時同步聲音旁白等等,此外 Opus 也可以透過降低編碼比特率,達成更低的算法延遲,最低可以到 5 ms。在多個聽覺盲測中,Opus 都比 MP3、AAC、HE-AAC 等常見格式,有更低的延遲和更好的聲音壓縮率。

比較不同聲音編碼格式的比特率、採樣率與延遲性

Opus 與其他流行音頻格式之間的編碼效率比較

所以,臨時用用可以考慮 Speex。如果要正式上線,還是考慮使用 Opus

Speex 的 JNI 實現

Speex 代碼可以從 Speex 官網 下載,目前最新版本爲 v1.2.0 (可能以後都不會更新了)

Speex 源碼是 C 語音版本的。

自己使用的時候,加了一套 API 方便調用。

// speex_codec.h
#include "speex/speex.h"

namespace speex {
    class SpeexCodec {
    public:
        enum CodecMode {
            ENCODE,         // 編碼,壓縮
            DECODE,         // 解碼,解壓
        };

        enum Quality {
            // [1 - 15]
            QUALITY_START,
            QUALITY_1,  QUALITY_2,  QUALITY_3,  QUALITY_4,  QUALITY_5,
            QUALITY_6,  QUALITY_7,  QUALITY_8,  QUALITY_9,  QUALITY_10,
            QUALITY_11, QUALITY_12, QUALITY_13, QUALITY_14, QUALITY_15,
            QUALITY_END,
        };

        enum Complexity {
            // [1 - 8]
            COMPLEXITY_START,
            COMPLEXITY_1, COMPLEXITY_2, COMPLEXITY_3, COMPLEXITY_4,
            COMPLEXITY_5, COMPLEXITY_6, COMPLEXITY_7, COMPLEXITY_8,
            COMPLEXITY_END,
        };

        enum ModeId {
            MODEID_NB  = SPEEX_MODEID_NB,   // narrowband 窄寬帶模式
            MODEID_WB  = SPEEX_MODEID_WB,   // wideband 寬帶模式
            MODEID_UWB = SPEEX_MODEID_UWB,  // wideband 超寬帶模式
        };

        SpeexCodec(CodecMode  codecMode,
                   int        decSize,
                   int        encSize,
                   Quality    quality,
                   Complexity complexity,
                   ModeId     modeId);
        ~SpeexCodec();

        int encode(short * dec, int decSize, int * useDecSize, char * enc);
        int decode(char * enc, int encSize, int * useEncSize, short * dec);
        void reset();

        CodecMode  getCodecMode();
        Quality    getQuality();
        Complexity getComplexity();
        ModeId     getModeId();
        int        getDecSize();
        int        getEncSize();

    private:
        SpeexCodec(){}
        SpeexCodec(const SpeexCodec&){}
        SpeexCodec(const SpeexCodec&&){}
        SpeexCodec& operator = (const SpeexCodec&){ return *this; }

        CodecMode  mCodecMode;
        Quality    mQuality;
        Complexity mComplexity;
        ModeId     mModeId;
        int        mDecSize;
        int        mEncSize;
        void *     mState;
        SpeexBits  mBits;
    };
}
// speex_codec.cpp
#include "speex_codec.h"

namespace speex {
    SpeexCodec::SpeexCodec(CodecMode  codecMode,
                           int        decSize,
                           int        encSize,
                           Quality    quality,
                           Complexity complexity,
                           ModeId     modeId) {
        this->mCodecMode  = codecMode;
        this->mDecSize    = decSize;
        this->mEncSize    = encSize;
        this->mQuality    = quality;
        this->mComplexity = complexity;
        this->mModeId     = modeId;

        if ((this->mQuality <= QUALITY_START || this->mQuality >= QUALITY_END)
            || (this->mDecSize <= 0)) {
            ASSERT_EXIT();
        }

        switch (this->mCodecMode) {
            case ENCODE:
                if (this->mComplexity <= COMPLEXITY_START || this->mComplexity >= COMPLEXITY_END) {
                    ASSERT_EXIT();
                }
                this->mState = speex_encoder_init(speex_lib_get_mode(this->mModeId));
                speex_encoder_ctl(this->mState, SPEEX_SET_QUALITY, &this->mQuality);
                speex_encoder_ctl(this->mState, SPEEX_SET_COMPLEXITY, &this->mComplexity);
                break;
            case DECODE:
                this->mState = speex_decoder_init(speex_lib_get_mode(this->mModeId));
                break;
            default:
                ASSERT_EXIT();
        }

        speex_bits_init(&this->mBits);
    }

    SpeexCodec::~SpeexCodec() {
        speex_bits_destroy(&this->mBits);
        switch (this->mCodecMode) {
            case ENCODE:
                speex_encoder_destroy(this->mState);
                break;
            case DECODE:
                speex_decoder_destroy(this->mState);
                break;
            default:
                ASSERT_EXIT();
        }
    }

    int SpeexCodec::encode(short * dec, int decSize, int * useDecSize, char * enc) {
        if (useDecSize != nullptr) {
            *useDecSize = 0;
        }
        if (dec == nullptr || enc == nullptr || decSize <= 0 || this->mDecSize <= 0
            || this->mCodecMode != ENCODE) {
            return -1;
        }
        int encLen = 0;
        {
            int n = decSize / this->mDecSize;
            if (n > 0) {
                int max_nbytes = (this->mEncSize > 0) ? this->mEncSize : this->mDecSize;
                for (int i = 0; i < n; ++ i) {
                    speex_bits_reset(&this->mBits);
                    speex_encode_int(this->mState, dec + i * this->mDecSize, &this->mBits);
                    encLen += speex_bits_write(&this->mBits, enc + encLen, max_nbytes);
                }
            }

            if (useDecSize != nullptr) {
                *useDecSize = n * this->mDecSize;
            }
        }
        return encLen;
    }

    int SpeexCodec::decode(char * enc, int encSize, int * useEncSize, short * dec) {
        if (useEncSize != nullptr) {
            *useEncSize = 0;
        }
        if (enc == nullptr || dec == nullptr || encSize <= 0 || this->mEncSize <= 0
            || this->mDecSize <= 0 || this->mCodecMode != DECODE) {
            return -1;
        }
        int decLen = 0;
        {
            int n = encSize / this->mEncSize;
            if (n > 0) {
                for (int i = 0; i < n; ++ i) {
                    speex_bits_reset(&this->mBits);
                    speex_bits_read_from(&this->mBits, enc + i * this->mEncSize, this->mEncSize);
                    speex_decode_int(this->mState, &this->mBits, dec + decLen);
                    decLen += this->mDecSize;
                }
            }

            if (useEncSize != nullptr) {
                *useEncSize = n * this->mEncSize;
            }
        }
        return decLen;
    }

    void SpeexCodec::reset() {
        int eph = 0;
        switch (this->mCodecMode) {
            case ENCODE:
                speex_encoder_ctl(this->mState, SPEEX_RESET_STATE, nullptr);
                break;
            case DECODE:
                speex_decoder_ctl(this->mState, SPEEX_RESET_STATE, nullptr);
                speex_decoder_ctl(this->mState, SPEEX_SET_ENH, &eph);
                break;
            default:
                ASSERT_EXIT();
        }
    }

    SpeexCodec::CodecMode SpeexCodec::getCodecMode() {
        return this->mCodecMode;
    }

    SpeexCodec::Quality SpeexCodec::getQuality() {
        return this->mQuality;
    }

    SpeexCodec::Complexity SpeexCodec::getComplexity() {
        return this->mComplexity;
    }

    SpeexCodec::ModeId SpeexCodec::getModeId() {
        return this->mModeId;
    }

    int SpeexCodec::getDecSize() {
        return this->mDecSize;
    }

    int SpeexCodec::getEncSize() {
        return this->mEncSize;
    }
}
#include <jni.h>
#include "speex_codec.h"

static speex::SpeexCodec * gmpSpeexCodecEncode = nullptr;
static speex::SpeexCodec * gmpSpeexCodecDecode = nullptr;
static bool gmSpeexCodecOpen = false;

extern "C"
JNIEXPORT jint JNICALL Java_org_zone_speex_Speex_JniOpen
  (JNIEnv *env, jclass , jint quality, jint complexity, jint modeId, jint decSize, jint encSize) {
    if (gmSpeexCodecOpen) {
        return (jint)-1;
    }

    gmSpeexCodecOpen = true;

    gmpSpeexCodecEncode = new speex::SpeexCodec(speex::SpeexCodec::ENCODE, decSize, encSize,
                                               (speex::SpeexCodec::Quality) quality,
                                               (speex::SpeexCodec::Complexity) complexity,
                                               (speex::SpeexCodec::ModeId) modeId);

    gmpSpeexCodecDecode = new speex::SpeexCodec(speex::SpeexCodec::DECODE, decSize, encSize,
                                               (speex::SpeexCodec::Quality) quality,
                                               (speex::SpeexCodec::Complexity) complexity,
                                               (speex::SpeexCodec::ModeId) modeId);

    if (gmpSpeexCodecEncode == nullptr || gmpSpeexCodecDecode == nullptr) {
        if (gmpSpeexCodecEncode != nullptr) {
            delete gmpSpeexCodecEncode;
            gmpSpeexCodecEncode = nullptr;
        }
        if (gmpSpeexCodecDecode != nullptr) {
            delete gmpSpeexCodecDecode;
            gmpSpeexCodecDecode = nullptr;
        }
        return -1;
    }

    return (jint)0;
}

extern "C"
JNIEXPORT jlong Java_org_zone_speex_Speex_JniEncode
    (JNIEnv *env, jclass, jshortArray jdec, jint jdecSize, jbyteArray jenc) {
    if (!gmSpeexCodecOpen || gmpSpeexCodecEncode == nullptr) {
        return -1;
    }
    jshort * dec = env->GetShortArrayElements(jdec, 0);
    jbyte * enc = env->GetByteArrayElements(jenc, 0);

    int useDecSize = 0, useEncSize = 0;
    useEncSize = gmpSpeexCodecEncode->encode((short *)dec, jdecSize, &useDecSize, (char *)enc);

    env->ReleaseShortArrayElements(jdec, dec, 0);
    env->ReleaseByteArrayElements(jenc, enc, 0);
    return (jlong)((((long)useDecSize) << 32L) | ((long)useEncSize & 0xFFFFFFFFL));
}


extern "C"
JNIEXPORT jlong Java_org_zone_speex_Speex_JniDecode
        (JNIEnv *env, jclass, jbyteArray jenc, jint jencSize, jshortArray jdec) {
    if (!gmSpeexCodecOpen || gmpSpeexCodecEncode == nullptr) {
        return -1;
    }
    jshort * dec = env->GetShortArrayElements(jdec, 0);
    jbyte * enc = env->GetByteArrayElements(jenc, 0);

    int useDecSize = 0, useEncSize = 0;
    useDecSize = gmpSpeexCodecDecode->decode((char *)enc, jencSize, &useEncSize, (short *)dec);

    env->ReleaseShortArrayElements(jdec, dec, 0);
    env->ReleaseByteArrayElements(jenc, enc, 0);
    return (jlong)((((long)useEncSize) << 32L) | ((long)useDecSize & 0xFFFFFFFFL));
}


extern "C"
JNIEXPORT jint JNICALL Java_org_zone_speex_Speex_JniClose
    (JNIEnv *env, jclass) {
    if (! gmSpeexCodecOpen) {
        LOGE("SpeexCodec not open !");
        return -1;
    }
    if (gmpSpeexCodecEncode != nullptr) {
        delete gmpSpeexCodecEncode;
        gmpSpeexCodecEncode = nullptr;
    }
    if (gmpSpeexCodecDecode == nullptr) {
        delete gmpSpeexCodecDecode;
        gmpSpeexCodecDecode = nullptr;
    }
    gmSpeexCodecOpen = false;
    return (jint)0;
}

extern "C"
JNIEXPORT jint JNICALL Java_org_zone_speex_Speex_JniReset
        (JNIEnv *env, jclass) {
    if (! gmSpeexCodecOpen) {
        LOGE("SpeexCodec not open !");
        return -1;
    }
    if (gmpSpeexCodecEncode != nullptr) {
        gmpSpeexCodecEncode->reset();
    }
    if (gmpSpeexCodecDecode == nullptr) {
        gmpSpeexCodecDecode->reset();
    }
    return (jint)0;
}

extern "C"
JNIEXPORT jint JNICALL Java_org_zone_speex_Speex_JniGetDecSize
        (JNIEnv *env, jclass) {
    int ret = -1;
    if (!gmSpeexCodecOpen || gmpSpeexCodecEncode == nullptr) {
        ;
    } else {
        ret = gmpSpeexCodecEncode->getDecSize();
    }
    return ret;
}

extern "C"
JNIEXPORT jint JNICALL Java_org_zone_speex_Speex_JniGetEncSize
        (JNIEnv *env, jclass) {
    int ret = -1;
    if (! gmSpeexCodecOpen || gmpSpeexCodecEncode == nullptr) {
        ;
    } else {
        ret = gmpSpeexCodecEncode->getEncSize();
    }
    return ret;
}

extern "C"
JNIEXPORT jint JNICALL Java_org_zone_speex_Speex_JniGetQuality
        (JNIEnv *env, jclass) {
    int ret = -1;
    if (! gmSpeexCodecOpen) {
        LOGE("SpeexCodec not open !");
    } else if (gmpSpeexCodecEncode == nullptr) {
        LOGE("SpeexCodecEncode invalid !");
    } else {
        ret = gmpSpeexCodecEncode->getQuality();
    }
    return ret;
}

extern "C"
JNIEXPORT jint JNICALL Java_org_zone_speex_Speex_JniGetComplexity
        (JNIEnv *env, jclass) {
    int ret = -1;
    if (! gmSpeexCodecOpen) {
        LOGE("SpeexCodec not open !");
    } else if (gmpSpeexCodecEncode == nullptr) {
        LOGE("SpeexCodecEncode invalid !");
    } else {
        ret = gmpSpeexCodecEncode->getComplexity();
    }
    return ret;
}

extern "C"
JNIEXPORT jint JNICALL Java_org_zone_speex_Speex_JniGetModeId
        (JNIEnv *env, jclass) {
    int ret = -1;
    if (! gmSpeexCodecOpen) {
        LOGE("SpeexCodec not open !");
    } else if (gmpSpeexCodecEncode == nullptr) {
        LOGE("SpeexCodecEncode invalid !");
    } else {
        ret = gmpSpeexCodecEncode->getModeId();
    }
    return ret;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章