android.speech.tts.TextToSpeech
android.speech.tts.TextToSpeechService
android.speech.tts.SynthesisCallback //自定义引擎回到的接口类
demo如下
private TextToSpeech textToSpeech;
private void testTTS() {
textToSpeech = new TextToSpeech(this, new TextToSpeech.OnInitListener() {
@Override
public void onInit(int status) {
if (status == TextToSpeech.SUCCESS) {
// setLanguage设置语言
int result = textToSpeech.setLanguage(Locale.CHINA);
textToSpeech.speak("寓言故事,老鼠和猫的故事,从前有一只老鼠,有一只猫",
TextToSpeech.QUEUE_FLUSH, null, "abc");
List engines = textToSpeech.getEngines();
Log.e(TAG, engines.size() + "/" + engines.get(0).toString());
}
}
});
textToSpeech.setOnUtteranceProgressListener(new UtteranceProgressListener() {
@Override
public void onStart(String utteranceId) {
Log.e(TAG, "onStart");
}
@Override
public void onDone(String utteranceId) {
Log.e(TAG, "onDone");
}
@Override
public void onError(String utteranceId) {
Log.e(TAG, "onError");
}
});
}
执行结果:执行该方法后,语音会播报内容,打印日志如下
2020-04-08 15:29:24.064 22831-22831/com.example.test E/speech: 1/EngineInfo{name=com.samsung.SMT}
2020-04-08 15:29:24.075 22831-22857/com.example.test E/speech: onStart
2020-04-08 15:29:29.946 22831-22857/com.example.test E/speech: onDone
总结:只有一个引擎,是com.samsung.SMT,语音播报前面会回调对应的方法;如果是其它手机会有差别,引用用的引擎不一样。举例:
三星 com.samsung.SMT
荣耀 com.iflytek.speechsuite
红米 com.svox.pico
原生板子 com.google.android.tts
TTS流程总结
Android SDK关于TTS的代码只是完成了接口的定义,而具体的引擎实现需要各产商单独定义
核心流程就是TextToSpeech开启服务和设置语音合成过程的回调
开启的服务需要继承TextToSpeechService,且action="android.intent.action.TTS_SERVICE",而服务就是自定义引擎的开始
初始化完成后,当调用speak或者synthesizeToFile功能后,最后调用自定义服务的onSynthesizeText(),语音成功过程,通过参数SynthesisCallback完成回调,形成闭环
源码分析
TextToSpeech构造函数如下
public TextToSpeech(Context context, OnInitListener listener, String engine,
String packageName, boolean useFallback) {
mContext = context;
mInitListener = listener;
mRequestedEngine = engine;
mUseFallback = useFallback;
mEarcons = new HashMap();
mUtterances = new HashMap();
mUtteranceProgressListener = null;
mEnginesHelper = new TtsEngines(mContext);
initTts();
}
紧接初始化tts, initTts()
android.speech.tts.TextToSpeech
private int initTts() {
// Step 1: Try connecting to the engine that was requested.
if (mRequestedEngine != null) {
if (mEnginesHelper.isEngineInstalled(mRequestedEngine)) {
if (connectToEngine(mRequestedEngine)) {
mCurrentEngine = mRequestedEngine;
return SUCCESS;
} else if (!mUseFallback) {
mCurrentEngine = null;
dispatchOnInit(ERROR);
return ERROR;
}
} else if (!mUseFallback) {
Log.i(TAG, "Requested engine not installed: " + mRequestedEngine);
mCurrentEngine = null;
dispatchOnInit(ERROR);
return ERROR;
}
}
// Step 2: Try connecting to the user's default engine.
final String defaultEngine = getDefaultEngine();
if (defaultEngine != null && !defaultEngine.equals(mRequestedEngine)) {
if (connectToEngine(defaultEngine)) {
mCurrentEngine = defaultEngine;
return SUCCESS;
}
}
// Step 3: Try connecting to the highest ranked engine in the
// system.
final String highestRanked = mEnginesHelper.getHighestRankedEngineName();
if (highestRanked != null && !highestRanked.equals(mRequestedEngine) &&
!highestRanked.equals(defaultEngine)) {
if (connectToEngine(highestRanked)) {
mCurrentEngine = highestRanked;
return SUCCESS;
}
}
// NOTE: The API currently does not allow the caller to query whether
// they are actually connected to any engine. This might fail for various
// reasons like if the user disables all her TTS engines.
mCurrentEngine = null;
dispatchOnInit(ERROR);
return ERROR;
}
android.speech.tts.TtsEngines
public String getHighestRankedEngineName() {
final List engines = getEngines();
if (engines.size() > 0 && engines.get(0).system) {
return engines.get(0).name;
}
return null;
}
首先尝试连接到用户请求的引擎,其次连接到用户默认的引擎,最后连接到排名最高的引擎(即引擎集合里最前面的引擎),连接引擎如下
android.speech.tts.TextToSpeech
private boolean connectToEngine(String engine) {
Connection connection = new Connection();
Intent intent = new Intent(Engine.INTENT_ACTION_TTS_SERVICE);
intent.setPackage(engine);
boolean bound = mContext.bindService(intent, connection, Context.BIND_AUTO_CREATE);
if (!bound) {
Log.e(TAG, "Failed to bind to " + engine);
return false;
} else {
Log.i(TAG, "Sucessfully bound to " + engine);
mConnectingServiceConnection = connection;
return true;
}
}
@SdkConstant(SdkConstantType.SERVICE_ACTION)
public static final String INTENT_ACTION_TTS_SERVICE =
"android.intent.action.TTS_SERVICE";
Engine.INTENT_ACTION_TTS_SERVICE的值是"android.intent.action.TTS_SERVICE";其连接的服务的action="android.intent.action.TTS_SERVICE",
且必须继承TxetToSpeechService,开启服务成功后,TTS引擎的初始化就完成了,接下来看什么时候开始回调new TextToSpeech.OnInitListener()
action="android.intent.action.TTS_SERVICE"服务开启后的相关流程查看Connection的onServiceConnected
private class Connection implements ServiceConnection {
.....
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
synchronized(mStartLock) {
mConnectingServiceConnection = null;
Log.i(TAG, "Connected to " + name);
if (mOnSetupConnectionAsyncTask != null) {
mOnSetupConnectionAsyncTask.cancel(false);
}
mService = ITextToSpeechService.Stub.asInterface(service);
mServiceConnection = Connection.this;
mEstablished = false;
mOnSetupConnectionAsyncTask = new SetupConnectionAsyncTask(name);
mOnSetupConnectionAsyncTask.execute();
}
}
....
}
android.speech.tts.TextToSpeechService
@Override
public IBinder onBind(Intent intent) {
if (TextToSpeech.Engine.INTENT_ACTION_TTS_SERVICE.equals(intent.getAction())) {
return mBinder;
}
return null;
}
获取到跨进程的服务返回的IBinder在TextToSpeechService服务中,对应的就是mService,其中跨进程的服务的还有执行异步任务
private class SetupConnectionAsyncTask extends AsyncTask {
private final ComponentName mName;
public SetupConnectionAsyncTask(ComponentName name) {
mName = name;
}
@Override
protected Integer doInBackground(Void... params) {
synchronized(mStartLock) {
if (isCancelled()) {
return null;
}
try {
mService.setCallback(getCallerIdentity(), mCallback);
if (mParams.getString(Engine.KEY_PARAM_LANGUAGE) == null) {
String[] defaultLanguage = mService.getClientDefaultLanguage();
mParams.putString(Engine.KEY_PARAM_LANGUAGE, defaultLanguage[0]);
mParams.putString(Engine.KEY_PARAM_COUNTRY, defaultLanguage[1]);
mParams.putString(Engine.KEY_PARAM_VARIANT, defaultLanguage[2]);
// Get the default voice for the locale.
String defaultVoiceName = mService.getDefaultVoiceNameFor(
defaultLanguage[0], defaultLanguage[1], defaultLanguage[2]);
mParams.putString(Engine.KEY_PARAM_VOICE_NAME, defaultVoiceName);
}
Log.i(TAG, "Set up connection to " + mName);
return SUCCESS;
} catch (RemoteException re) {
Log.e(TAG, "Error connecting to service, setCallback() failed");
return ERROR;
}
}
}
@Override
protected void onPostExecute(Integer result) {
synchronized(mStartLock) {
if (mOnSetupConnectionAsyncTask == this) {
mOnSetupConnectionAsyncTask = null;
}
mEstablished = true;
dispatchOnInit(result);
}
}
}
private void dispatchOnInit(int result) {
synchronized (mStartLock) {
if (mInitListener != null) {
mInitListener.onInit(result);
mInitListener = null;
}
}
}
private final ITextToSpeechCallback.Stub mCallback =
new ITextToSpeechCallback.Stub() {
public void onStop(String utteranceId, boolean isStarted)
throws RemoteException {
UtteranceProgressListener listener = mUtteranceProgressListener;
if (listener != null) {
listener.onStop(utteranceId, isStarted);
}
};
@Override
public void onSuccess(String utteranceId) {
UtteranceProgressListener listener = mUtteranceProgressListener;
if (listener != null) {
listener.onDone(utteranceId);
}
}
@Override
public void onError(String utteranceId, int errorCode) {
UtteranceProgressListener listener = mUtteranceProgressListener;
if (listener != null) {
listener.onError(utteranceId);
}
}
@Override
public void onStart(String utteranceId) {
UtteranceProgressListener listener = mUtteranceProgressListener;
if (listener != null) {
listener.onStart(utteranceId);
}
}
@Override
public void onBeginSynthesis(
String utteranceId,
int sampleRateInHz,
int audioFormat,
int channelCount) {
UtteranceProgressListener listener = mUtteranceProgressListener;
if (listener != null) {
listener.onBeginSynthesis(
utteranceId, sampleRateInHz, audioFormat, channelCount);
}
}
@Override
public void onAudioAvailable(String utteranceId, byte[] audio) {
UtteranceProgressListener listener = mUtteranceProgressListener;
if (listener != null) {
listener.onAudioAvailable(utteranceId, audio);
}
}
@Override
public void onRangeStart(String utteranceId, int start, int end, int frame) {
UtteranceProgressListener listener = mUtteranceProgressListener;
if (listener != null) {
listener.onRangeStart(utteranceId, start, end, frame);
}
}
};
一个是给TTS服务设置回调,回到设置成功后,执行dispatchOnInit(result)代表引擎设置回调成功,且TextToSpeech初始化成功
其中给TTS服务设置的回调就是语音合成相关事件的侦听器,就是TextToSpeech的setOnUtteranceProgressListener
TTS初始化完成后,就可以直接调用speak,synthesizeToFile,下面流程按speak分析speak源码如下
public int speak(final CharSequence text,
final int queueMode,
final Bundle params,
final String utteranceId) {
return runAction(new Action() {
@Override
public Integer run(ITextToSpeechService service) throws RemoteException {
Uri utteranceUri = mUtterances.get(text);
if (utteranceUri != null) {
return service.playAudio(getCallerIdentity(), utteranceUri, queueMode,
getParams(params), utteranceId);
} else {
return service.speak(getCallerIdentity(), text, queueMode, getParams(params),
utteranceId);
}
}
}, ERROR, "speak");
}
其中调用runAction方法,最后调用
public R runAction(Action action, R errorResult, String method,
boolean reconnect, boolean onlyEstablishedConnection) {
synchronized (mStartLock) {
try {
if (mService == null) {
Log.w(TAG, method + " failed: not connected to TTS engine");
return errorResult;
}
if (onlyEstablishedConnection && !isEstablished()) {
Log.w(TAG, method + " failed: TTS engine connection not fully set up");
return errorResult;
}
return action.run(mService);
} catch (RemoteException ex) {
Log.e(TAG, method + " failed", ex);
if (reconnect) {
disconnect();
initTts();
}
return errorResult;
}
}
}
其中mService就是跨进程的服务,本过程主要是检查相关服务是否为null,最后还是调用跨进程服务的speak方法,接下来查看TextToSpeechServcie的mBinder的speak方法
private final ITextToSpeechService.Stub mBinder =
new ITextToSpeechService.Stub() {
@Override
public int speak(
IBinder caller,
CharSequence text,
int queueMode,
Bundle params,
String utteranceId) {
if (!checkNonNull(caller, text, params)) {
return TextToSpeech.ERROR;
}
SpeechItem item =
new SynthesisSpeechItem(
caller,
Binder.getCallingUid(),
Binder.getCallingPid(),
params,
utteranceId,
text);
return mSynthHandler.enqueueSpeechItem(queueMode, item);
}
@Override
public int synthesizeToFileDescriptor(
IBinder caller,
CharSequence text,
ParcelFileDescriptor fileDescriptor,
Bundle params,
String utteranceId) {
if (!checkNonNull(caller, text, fileDescriptor, params)) {
return TextToSpeech.ERROR;
}
// In test env, ParcelFileDescriptor instance may be EXACTLY the same
// one that is used by client. And it will be closed by a client, thus
// preventing us from writing anything to it.
final ParcelFileDescriptor sameFileDescriptor =
ParcelFileDescriptor.adoptFd(fileDescriptor.detachFd());
SpeechItem item =
new SynthesisToFileOutputStreamSpeechItem(
caller,
Binder.getCallingUid(),
Binder.getCallingPid(),
params,
utteranceId,
text,
new ParcelFileDescriptor.AutoCloseOutputStream(
sameFileDescriptor));
return mSynthHandler.enqueueSpeechItem(TextToSpeech.QUEUE_ADD, item);
}
@Override
public int playAudio(
IBinder caller,
Uri audioUri,
int queueMode,
Bundle params,
String utteranceId) {
if (!checkNonNull(caller, audioUri, params)) {
return TextToSpeech.ERROR;
}
SpeechItem item =
new AudioSpeechItem(
caller,
Binder.getCallingUid(),
Binder.getCallingPid(),
params,
utteranceId,
audioUri);
return mSynthHandler.enqueueSpeechItem(queueMode, item);
}
@Override
public int playSilence(
IBinder caller, long duration, int queueMode, String utteranceId) {
if (!checkNonNull(caller)) {
return TextToSpeech.ERROR;
}
SpeechItem item =
new SilenceSpeechItem(
caller,
Binder.getCallingUid(),
Binder.getCallingPid(),
utteranceId,
duration);
return mSynthHandler.enqueueSpeechItem(queueMode, item);
}
@Override
public boolean isSpeaking() {
return mSynthHandler.isSpeaking() || mAudioPlaybackHandler.isSpeaking();
}
@Override
public int stop(IBinder caller) {
if (!checkNonNull(caller)) {
return TextToSpeech.ERROR;
}
return mSynthHandler.stopForApp(caller);
}
@Override
public String[] getLanguage() {
return onGetLanguage();
}
@Override
public String[] getClientDefaultLanguage() {
return getSettingsLocale();
}
@Override
public int isLanguageAvailable(String lang, String country, String variant) {
if (!checkNonNull(lang)) {
return TextToSpeech.ERROR;
}
return onIsLanguageAvailable(lang, country, variant);
}
@Override
public String[] getFeaturesForLanguage(
String lang, String country, String variant) {
Set features = onGetFeaturesForLanguage(lang, country, variant);
String[] featuresArray = null;
if (features != null) {
featuresArray = new String[features.size()];
features.toArray(featuresArray);
} else {
featuresArray = new String[0];
}
return featuresArray;
}
@Override
public int loadLanguage(
IBinder caller, String lang, String country, String variant) {
if (!checkNonNull(lang)) {
return TextToSpeech.ERROR;
}
int retVal = onIsLanguageAvailable(lang, country, variant);
if (retVal == TextToSpeech.LANG_AVAILABLE
|| retVal == TextToSpeech.LANG_COUNTRY_AVAILABLE
|| retVal == TextToSpeech.LANG_COUNTRY_VAR_AVAILABLE) {
SpeechItem item =
new LoadLanguageItem(
caller,
Binder.getCallingUid(),
Binder.getCallingPid(),
lang,
country,
variant);
if (mSynthHandler.enqueueSpeechItem(TextToSpeech.QUEUE_ADD, item)
!= TextToSpeech.SUCCESS) {
return TextToSpeech.ERROR;
}
}
return retVal;
}
@Override
public List getVoices() {
return onGetVoices();
}
@Override
public int loadVoice(IBinder caller, String voiceName) {
if (!checkNonNull(voiceName)) {
return TextToSpeech.ERROR;
}
int retVal = onIsValidVoiceName(voiceName);
if (retVal == TextToSpeech.SUCCESS) {
SpeechItem item =
new LoadVoiceItem(
caller,
Binder.getCallingUid(),
Binder.getCallingPid(),
voiceName);
if (mSynthHandler.enqueueSpeechItem(TextToSpeech.QUEUE_ADD, item)
!= TextToSpeech.SUCCESS) {
return TextToSpeech.ERROR;
}
}
return retVal;
}
public String getDefaultVoiceNameFor(String lang, String country, String variant) {
if (!checkNonNull(lang)) {
return null;
}
int retVal = onIsLanguageAvailable(lang, country, variant);
if (retVal == TextToSpeech.LANG_AVAILABLE
|| retVal == TextToSpeech.LANG_COUNTRY_AVAILABLE
|| retVal == TextToSpeech.LANG_COUNTRY_VAR_AVAILABLE) {
return onGetDefaultVoiceNameFor(lang, country, variant);
} else {
return null;
}
}
@Override
public void setCallback(IBinder caller, ITextToSpeechCallback cb) {
// Note that passing in a null callback is a valid use case.
if (!checkNonNull(caller)) {
return;
}
mCallbacks.setCallback(caller, cb);
}
private String intern(String in) {
// The input parameter will be non null.
return in.intern();
}
private boolean checkNonNull(Object... args) {
for (Object o : args) {
if (o == null) return false;
}
return true;
}
};
里面对应的方法都是textToSpeech调用的方法,跟着流程走,我们查看speak方法,其中传递的SpeechItem实例SynthesisSpeechItem
public int enqueueSpeechItem(int queueMode, final SpeechItem speechItem) {
UtteranceProgressDispatcher utterenceProgress = null;
if (speechItem instanceof UtteranceProgressDispatcher) {
utterenceProgress = (UtteranceProgressDispatcher) speechItem;
}
if (!speechItem.isValid()) {
if (utterenceProgress != null) {
utterenceProgress.dispatchOnError(
TextToSpeech.ERROR_INVALID_REQUEST);
}
return TextToSpeech.ERROR;
}
if (queueMode == TextToSpeech.QUEUE_FLUSH) {
stopForApp(speechItem.getCallerIdentity());
} else if (queueMode == TextToSpeech.QUEUE_DESTROY) {
stopAll();
}
Runnable runnable = new Runnable() {
@Override
public void run() {
if (setCurrentSpeechItem(speechItem)) {
speechItem.play();
removeCurrentSpeechItem();
} else {
// The item is alreadly flushed. Stopping.
speechItem.stop();
}
}
};
Message msg = Message.obtain(this, runnable);
// The obj is used to remove all callbacks from the given app in
// stopForApp(String).
//
// Note that this string is interned, so the == comparison works.
msg.obj = speechItem.getCallerIdentity();
if (sendMessage(msg)) {
return TextToSpeech.SUCCESS;
} else {
Log.w(TAG, "SynthThread has quit");
if (utterenceProgress != null) {
utterenceProgress.dispatchOnError(TextToSpeech.ERROR_SERVICE);
}
return TextToSpeech.ERROR;
}
}
一种是根据排队模式停止当前App的语音或者所有App的语音,然后执行speechItem.paly()
public void play() {
synchronized (this) {
if (mStarted) {
throw new IllegalStateException("play() called twice");
}
mStarted = true;
}
playImpl();
}
protected abstract void playImpl();
随后调用speechItem.实现类的playImpl,实现类是SynthesisSpeechItem,它的playImpl方法如下
@Override
protected void playImpl() {
AbstractSynthesisCallback synthesisCallback;
mEventLogger.onRequestProcessingStart();
synchronized (this) {
// stop() might have been called before we enter this
// synchronized block.
if (isStopped()) {
return;
}
mSynthesisCallback = createSynthesisCallback();
synthesisCallback = mSynthesisCallback;
}
TextToSpeechService.this.onSynthesizeText(mSynthesisRequest, synthesisCallback);
// Fix for case where client called .start() & .error(), but did not called .done()
if (synthesisCallback.hasStarted() && !synthesisCallback.hasFinished()) {
synthesisCallback.done();
}
}
protected AbstractSynthesisCallback createSynthesisCallback() {
return new PlaybackSynthesisCallback(getAudioParams(),
mAudioPlaybackHandler, this, getCallerIdentity(), mEventLogger, false);
}
protected abstract void onSynthesizeText(SynthesisRequest request, SynthesisCallback callback);
一个是创建回调,一个是调用onSyntehsizeText方法;核心方法就在这,参数一个是请求的所有参数,一个是回调,但是是TextToSpeechService的抽象方法;然后看回调对象
android.speech.tts.SynthesisCallback
int start(
int sampleRateInHz,
@SupportedAudioFormat int audioFormat,
@IntRange(from = 1, to = 2) int channelCount);
int audioAvailable(byte[] buffer, int offset, int length);
int done();
android.speech.tts.PlaybackSynthesisCallback
@Override
public int audioAvailable(byte[] buffer, int offset, int length) {
if (DBG) Log.d(TAG, "audioAvailable(byte[" + buffer.length + "]," + offset + "," + length
+ ")");
if (length > getMaxBufferSize() || length <= 0) {
throw new IllegalArgumentException("buffer is too large or of zero length (" +
+ length + " bytes)");
}
SynthesisPlaybackQueueItem item = null;
synchronized (mStateLock) {
if (mItem == null) {
mStatusCode = TextToSpeech.ERROR_OUTPUT;
return TextToSpeech.ERROR;
}
if (mStatusCode != TextToSpeech.SUCCESS) {
if (DBG) Log.d(TAG, "Error was raised");
return TextToSpeech.ERROR;
}
if (mStatusCode == TextToSpeech.STOPPED) {
return errorCodeOnStop();
}
item = mItem;
}
// Sigh, another copy.
final byte[] bufferCopy = new byte[length];
System.arraycopy(buffer, offset, bufferCopy, 0, length);
mDispatcher.dispatchOnAudioAvailable(bufferCopy);
// Might block on mItem.this, if there are too many buffers waiting to
// be consumed.
try {
item.put(bufferCopy);
} catch (InterruptedException ie) {
synchronized (mStateLock) {
mStatusCode = TextToSpeech.ERROR_OUTPUT;
return TextToSpeech.ERROR;
}
}
mLogger.onEngineDataReceived();
return TextToSpeech.SUCCESS;
}
@Override
public int done() {
if (DBG) Log.d(TAG, "done()");
int statusCode = 0;
SynthesisPlaybackQueueItem item = null;
synchronized (mStateLock) {
if (mDone) {
Log.w(TAG, "Duplicate call to done()");
// Not an error that would prevent synthesis. Hence no
// setStatusCode
return TextToSpeech.ERROR;
}
if (mStatusCode == TextToSpeech.STOPPED) {
if (DBG) Log.d(TAG, "Request has been aborted.");
return errorCodeOnStop();
}
mDone = true;
if (mItem == null) {
// .done() was called before .start. Treat it as successful synthesis
// for a client, despite service bad implementation.
Log.w(TAG, "done() was called before start() call");
if (mStatusCode == TextToSpeech.SUCCESS) {
mDispatcher.dispatchOnSuccess();
} else {
mDispatcher.dispatchOnError(mStatusCode);
}
mLogger.onEngineComplete();
return TextToSpeech.ERROR;
}
item = mItem;
statusCode = mStatusCode;
}
所以真正的实现是在它的子类里实现,在语音引擎完成过程,必须回调对应的方法,后续我们只跟踪对应的done()方法,最后通过mDispatcher.dispatchOnSuccess()继续回调,跟踪最后调用的是TextToSpeech设置的监听结束
@Override
public void onSuccess(String utteranceId) {
UtteranceProgressListener listener = mUtteranceProgressListener;
if (listener != null) {
listener.onDone(utteranceId);
}
}
至此整个TTS服务整个调用过程结束
自定义引擎服务--完成文本转语音的功能,且完成对应的回调public class TtsService extends TextToSpeechService {
@Override
protected int onIsLanguageAvailable(String lang, String country, String variant) {
return 0;
}
@Override
protected String[] onGetLanguage() {
return new String[0];
}
@Override
protected int onLoadLanguage(String lang, String country, String variant) {
return 0;
}
@Override
protected void onStop() {
}
@Override
protected void onSynthesizeText(SynthesisRequest request, SynthesisCallback callback) {
}
}
继承的服务必须设置action="android.intent.action.TTS_SERVICE",且方法onSynthesizeText()是定义引擎开始的地方,而完成回到需要使用参数SynthesisCallback完成闭环,里面涉及跟C语言的交互完成,中间的过程可以参考博客含有TTS引擎实现的流程
作者:小马二号