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 的主要特性归纳如下:
目前只测了部分压缩的性能(设备:一加3T【高通骁龙821】)
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 等常见格式,有更低的延迟和更好的声音压缩率。
所以,临时用用可以考虑 Speex。如果要正式上线,还是考虑使用 Opus。
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;
}