版本記錄
版本號(hào) | 時(shí)間 |
---|---|
V1.0 | 2017.12.28 |
前言
ios系統(tǒng)中有很多方式可以播放音頻文件赚爵,這里我們就詳細(xì)的說(shuō)明下播放音樂(lè)文件的原理和實(shí)例。感興趣的可以看我寫(xiě)的上面幾篇容为。
1. 幾種播放音頻文件的方式(一) —— 播放本地音樂(lè)
2. 幾種播放音頻文件的方式(二) —— 音效播放
3. 幾種播放音頻文件的方式(三) —— 網(wǎng)絡(luò)音樂(lè)播放
4. 幾種播放音頻文件的方式(四) —— 音頻隊(duì)列服務(wù)(Audio Queue Services)(一)
5. 幾種播放音頻文件的方式(五) —— 音頻隊(duì)列服務(wù)(Audio Queue Services)簡(jiǎn)介(二)
6. 幾種播放音頻文件的方式(六) —— 音頻隊(duì)列服務(wù)(Audio Queue Services)之關(guān)于音頻隊(duì)列(三)
7. 幾種播放音頻文件的方式(七) —— 音頻隊(duì)列服務(wù)(Audio Queue Services)之錄制音頻(四)
Playing Audio - 播放音頻
使用音頻隊(duì)列服務(wù)播放音頻時(shí),音源可以是任何東西 - 磁盤(pán)上文件,基于軟件的音頻合成器态坦,內(nèi)存中的對(duì)象等等。本章介紹最常見(jiàn)的情況:播放磁盤(pán)上的文件棒拂。
注意:本章介紹了一個(gè)基于ANSI-C的播放實(shí)現(xiàn)伞梯,并且使用了
Mac OS X Core Audio SDK
中的C ++類(lèi)。對(duì)于基于Objective-C的示例帚屉,請(qǐng)參閱iOS Dev Center的SpeakHere
示例代碼谜诫。
要為您的應(yīng)用程序添加播放功能,通常需要執(zhí)行以下步驟:
- 定義一個(gè)自定義結(jié)構(gòu)來(lái)管理狀態(tài)攻旦,格式和路徑信息喻旷。
- 編寫(xiě)音頻隊(duì)列回調(diào)函數(shù)來(lái)執(zhí)行實(shí)際播放。
- 編寫(xiě)代碼來(lái)確定音頻隊(duì)列緩沖區(qū)的大小牢屋。
- 打開(kāi)音頻文件進(jìn)行播放并確定其音頻數(shù)據(jù)格式掰邢。
- 創(chuàng)建一個(gè)播放音頻隊(duì)列并將其配置為播放牺陶。
- 分配和入隊(duì)音頻隊(duì)列緩沖區(qū)。告訴音頻隊(duì)列開(kāi)始播放辣之。完成后,回放回調(diào)告知音頻隊(duì)列停止皱炉。
- 銷(xiāo)毀音頻隊(duì)列怀估,釋放資源。
本章的其余部分將詳細(xì)介紹這些步驟合搅。
Define a Custom Structure to Manage State - 定義一個(gè)自定義結(jié)構(gòu)管理狀態(tài)
首先多搀,定義一個(gè)用來(lái)管理音頻格式和音頻隊(duì)列狀態(tài)信息的自定義結(jié)構(gòu)。 Listing 3-1
說(shuō)明了這樣一個(gè)結(jié)構(gòu):
// Listing 3-1 A custom structure for a playback audio queue
static const int kNumberBuffers = 3; // 1
struct AQPlayerState {
AudioStreamBasicDescription mDataFormat; // 2
AudioQueueRef mQueue; // 3
AudioQueueBufferRef mBuffers[kNumberBuffers]; // 4
AudioFileID mAudioFile; // 5
UInt32 bufferByteSize; // 6
SInt64 mCurrentPacket; // 7
UInt32 mNumPacketsToRead; // 8
AudioStreamPacketDescription *mPacketDescs; // 9
bool mIsRunning; // 10
};
此結(jié)構(gòu)中的大多數(shù)字段與用于記錄的自定義結(jié)構(gòu)中的字段相同(或接近)灾部,如Define a Custom Structure to Manage State中的“錄制音頻”章節(jié)中所述康铭。例如,mDataFormat
字段在此用于保存正在播放的文件的格式赌髓。錄制時(shí)从藤,類(lèi)似字段保存正在寫(xiě)入磁盤(pán)的文件的格式。
以下是這個(gè)結(jié)構(gòu)中的字段的描述:
- 設(shè)置要使用的音頻隊(duì)列緩沖區(qū)的數(shù)量锁蠕。如Audio Queue Buffers中所述夷野,“三”通常是一個(gè)很好的數(shù)字。
- 表示正在播放的文件的音頻數(shù)據(jù)格式的
AudioStreamBasicDescription
結(jié)構(gòu)(來(lái)自CoreAudioTypes.h
)荣倾。該格式被mQueue
字段中指定的音頻隊(duì)列使用悯搔。通過(guò)查詢(xún)音頻文件的kAudioFilePropertyDataFormat
屬性來(lái)填充mDataFormat
字段,如Obtaining a File’s Audio Data Format中所述舌仍。有關(guān)AudioStreamBasicDescription
結(jié)構(gòu)的詳細(xì)信息妒貌,請(qǐng)參閱Core Audio Data Types Reference。
- 表示正在播放的文件的音頻數(shù)據(jù)格式的
- 由應(yīng)用程序創(chuàng)建的播放音頻隊(duì)列铸豁。
- 一個(gè)數(shù)組灌曙,指向由音頻隊(duì)列管理的音頻隊(duì)列緩沖區(qū)的指針。
- 音頻文件對(duì)象推姻,表示您的程序播放的音頻文件平匈。
- 每個(gè)音頻隊(duì)列緩沖區(qū)的大小(以字節(jié)為單位)藏古。在創(chuàng)建音頻隊(duì)列之后并在啟動(dòng)之前增炭,在
DeriveBufferSize
函數(shù)的這些示例中計(jì)算此值。請(qǐng)參閱 Write a Function to Derive Playback Audio Queue Buffer Size拧晕。
- 每個(gè)音頻隊(duì)列緩沖區(qū)的大小(以字節(jié)為單位)藏古。在創(chuàng)建音頻隊(duì)列之后并在啟動(dòng)之前增炭,在
- 從音頻文件播放下一個(gè)數(shù)據(jù)包的數(shù)據(jù)包索引隙姿。
- 每次調(diào)用音頻隊(duì)列的回放回調(diào)時(shí)讀取的數(shù)據(jù)包數(shù)量。與
bufferByteSize
字段類(lèi)似厂捞,在創(chuàng)建音頻隊(duì)列之后并在啟動(dòng)之前输玷,在DeriveBufferSize
函數(shù)的這些示例中計(jì)算此值队丝。
- 每次調(diào)用音頻隊(duì)列的回放回調(diào)時(shí)讀取的數(shù)據(jù)包數(shù)量。與
- 對(duì)于
VBR
音頻數(shù)據(jù),正在播放的文件的數(shù)據(jù)包描述數(shù)組欲鹏。對(duì)于CBR
數(shù)據(jù)机久,該字段的值為NULL。
- 對(duì)于
- 指示音頻隊(duì)列是否正在運(yùn)行的布爾值赔嚎。
Write a Playback Audio Queue Callback - 編寫(xiě)播放音頻隊(duì)列回調(diào)
接下來(lái)膘盖,寫(xiě)一個(gè)回放音頻隊(duì)列回調(diào)函數(shù)。 這個(gè)回調(diào)有三個(gè)主要的事情:
- 從音頻文件讀取指定數(shù)量的數(shù)據(jù)并將其放入音頻隊(duì)列緩沖區(qū)
- 將音頻隊(duì)列緩沖區(qū)排入緩沖隊(duì)列
- 當(dāng)沒(méi)有更多的數(shù)據(jù)要從音頻文件讀取尤误,告訴音頻隊(duì)列停止
本部分顯示了一個(gè)示例回調(diào)聲明侠畔,分別描述了這些任務(wù),并最終呈現(xiàn)整個(gè)回放回調(diào)损晤。 有關(guān)回放回調(diào)角色的說(shuō)明软棺,請(qǐng)參閱圖Figure 1-4。
1. The Playback Audio Queue Callback Declaration - 播放音頻隊(duì)列回調(diào)函數(shù)聲明
Listing 3-2
顯示了一個(gè)播放音頻隊(duì)列回調(diào)函數(shù)的示例聲明尤勋,聲明為AudioQueue.h
頭文件中的AudioQueueOutputCallback
// Listing 3-2 The playback audio queue callback declaration
static void HandleOutputBuffer (
void *aqData, // 1
AudioQueueRef inAQ, // 2
AudioQueueBufferRef inBuffer // 3
)
以下是這段代碼的工作原理:
- 通常喘落,
aqData
是包含音頻隊(duì)列狀態(tài)信息的自定義結(jié)構(gòu),如Define a Custom Structure to Manage State所述斥黑。
- 通常喘落,
- 擁有此回調(diào)的音頻隊(duì)列揖盘。
- 一個(gè)音頻隊(duì)列緩沖區(qū),回調(diào)將通過(guò)讀取音頻文件來(lái)填充數(shù)據(jù)锌奴。
2. Reading From a File into an Audio Queue Buffer - 從文件中讀取數(shù)據(jù)到音頻隊(duì)列緩沖區(qū)
回放音頻隊(duì)列回調(diào)的第一個(gè)動(dòng)作是從音頻文件讀取數(shù)據(jù)并將其放入音頻隊(duì)列緩沖區(qū)兽狭。Listing 3-3
顯示了如何做到這一點(diǎn)。
// Listing 3-3 Reading from an audio file into an audio queue buffer
AudioFileReadPackets ( // 1
pAqData->mAudioFile, // 2
false, // 3
&numBytesReadFromFile, // 4
pAqData->mPacketDescs, // 5
pAqData->mCurrentPacket, // 6
&numPackets, // 7
inBuffer->mAudioData // 8
);
以下是這段代碼的工作原理:
- 在
AudioFile.h
頭文件中聲明的AudioFileReadPackets
函數(shù)從音頻文件讀取數(shù)據(jù)并將其放入緩沖區(qū)鹿蜀。
- 在
- 要從中讀取的音頻文件箕慧。
- 使用值為false來(lái)指示函數(shù)在讀取時(shí)不應(yīng)該緩存數(shù)據(jù)。
- 輸出時(shí)茴恰,從音頻文件中讀取的音頻數(shù)據(jù)的字節(jié)數(shù)颠焦。
- 在輸出上,從音頻文件中讀取數(shù)據(jù)的數(shù)據(jù)包描述數(shù)組往枣。 對(duì)于CBR數(shù)據(jù)伐庭,此參數(shù)的輸入值為NULL。
- 從音頻文件中讀取的第一個(gè)數(shù)據(jù)包數(shù)據(jù)包索引分冈。
- 輸入時(shí)圾另,從音頻文件中讀取的數(shù)據(jù)包數(shù)量。 輸出時(shí)雕沉,實(shí)際讀取的數(shù)據(jù)包數(shù)量集乔。
- 輸出時(shí),填充的音頻隊(duì)列緩沖區(qū)包含從音頻文件中讀取的數(shù)據(jù)坡椒。
3. Enqueuing an Audio Queue Buffer - 聲頻隊(duì)列緩沖入隊(duì)
現(xiàn)在扰路,數(shù)據(jù)已經(jīng)從音頻文件中讀取并放入音頻隊(duì)列緩沖區(qū)尤溜,回調(diào)將緩沖區(qū)排入隊(duì)列中,如Listing 3-4
所示汗唱。 一旦進(jìn)入緩沖區(qū)隊(duì)列宫莱,緩沖區(qū)中的音頻數(shù)據(jù)就可供音頻隊(duì)列發(fā)送到輸出設(shè)備。
// Listing 3-4 Enqueuing an audio queue buffer after reading from disk
AudioQueueEnqueueBuffer ( // 1
pAqData->mQueue, // 2
inBuffer, // 3
(pAqData->mPacketDescs ? numPackets : 0), // 4
pAqData->mPacketDescs // 5
);
以下是這段代碼的工作原理:
-
AudioQueueEnqueueBuffer
函數(shù)將音頻隊(duì)列緩沖區(qū)添加到緩沖區(qū)隊(duì)列中哩罪。
-
- 擁有緩沖隊(duì)列
buffer queue
的音頻隊(duì)列audio queue
梢睛。
- 擁有緩沖隊(duì)列
- 要排隊(duì)的音頻隊(duì)列緩沖區(qū)
- 音頻隊(duì)列緩沖區(qū)數(shù)據(jù)中表示的數(shù)據(jù)包數(shù)量。 對(duì)于不使用數(shù)據(jù)包描述的CBR數(shù)據(jù)识椰,使用0。
- 對(duì)于使用數(shù)據(jù)包描述的壓縮音頻數(shù)據(jù)格式深碱,緩沖區(qū)中數(shù)據(jù)包的數(shù)據(jù)包描述腹鹉。
4. Stopping an Audio Queue - 停止音頻隊(duì)列
你回調(diào)的最后一件事是檢查是否沒(méi)有更多的數(shù)據(jù)從你正在播放的音頻文件中讀取。 一旦發(fā)現(xiàn)文件結(jié)束敷硅,你的回調(diào)告訴播放音頻隊(duì)列停止功咒。 Listing 3-5
說(shuō)明了這一點(diǎn)。
// Listing 3-5 Stopping an audio queue
if (numPackets == 0) { // 1
AudioQueueStop ( // 2
pAqData->mQueue, // 3
false // 4
);
pAqData->mIsRunning = false; // 5
}
以下是這段代碼的工作原理:
- 檢查
AudioFileReadPackets
函數(shù)讀取的數(shù)據(jù)包的數(shù)量是否為0绞蹦。
- 檢查
-
AudioQueueStop
函數(shù)停止音頻隊(duì)列力奋。
-
- 要停止的音頻隊(duì)列。
- 當(dāng)所有排隊(duì)的緩沖區(qū)都被播放時(shí)幽七,異步停止音頻隊(duì)列景殷。 請(qǐng)參閱Audio Queue Control and State。
- 在自定義結(jié)構(gòu)中設(shè)置一個(gè)標(biāo)志來(lái)指示播放完成澡屡。
5. A Full Playback Audio Queue Callback - 播放音頻隊(duì)列回調(diào)函數(shù)完整版
Listing 3-6
顯示了完整播放音頻隊(duì)列回調(diào)的基本版本猿挚。 與本文檔中的其他代碼示例一樣,此列表不包括錯(cuò)誤處理驶鹉。
// Listing 3-6 A playback audio queue callback function
static void HandleOutputBuffer (
void *aqData,
AudioQueueRef inAQ,
AudioQueueBufferRef inBuffer
) {
AQPlayerState *pAqData = (AQPlayerState *) aqData; // 1
if (pAqData->mIsRunning == 0) return; // 2
UInt32 numBytesReadFromFile; // 3
UInt32 numPackets = pAqData->mNumPacketsToRead; // 4
AudioFileReadPackets (
pAqData->mAudioFile,
false,
&numBytesReadFromFile,
pAqData->mPacketDescs,
pAqData->mCurrentPacket,
&numPackets,
inBuffer->mAudioData
);
if (numPackets > 0) { // 5
inBuffer->mAudioDataByteSize = numBytesReadFromFile; // 6
AudioQueueEnqueueBuffer (
pAqData->mQueue,
inBuffer,
(pAqData->mPacketDescs ? numPackets : 0),
pAqData->mPacketDescs
);
pAqData->mCurrentPacket += numPackets; // 7
} else {
AudioQueueStop (
pAqData->mQueue,
false
);
pAqData->mIsRunning = false;
}
}
以下是這段代碼的工作原理:
- 實(shí)例化時(shí)提供給音頻隊(duì)列的自定義數(shù)據(jù)绩蜻,包括表示要播放的文件的音頻文件對(duì)象(類(lèi)型
AudioFileID
)以及各種狀態(tài)數(shù)據(jù)。 請(qǐng)參閱 Define a Custom Structure to Manage State室埋。
- 實(shí)例化時(shí)提供給音頻隊(duì)列的自定義數(shù)據(jù)绩蜻,包括表示要播放的文件的音頻文件對(duì)象(類(lèi)型
- 如果音頻隊(duì)列停止办绝,立即返回。
- 保存正在播放的文件中讀取的音頻數(shù)據(jù)字節(jié)數(shù)的變量姚淆。
- 使用要從正在播放的文件中讀取的數(shù)據(jù)包數(shù)初始化
numPackets
變量孕蝉。
- 使用要從正在播放的文件中讀取的數(shù)據(jù)包數(shù)初始化
- 測(cè)試是否從文件中檢索到某些音頻數(shù)據(jù)。 如果是肉盹,排隊(duì)新填充的緩沖區(qū)昔驱。 如果不是,則停止音頻隊(duì)列上忍。
- 告訴音頻隊(duì)列緩沖區(qū)結(jié)構(gòu)讀取數(shù)據(jù)的字節(jié)數(shù)骤肛。
- 根據(jù)讀取的數(shù)據(jù)包數(shù)量遞增數(shù)據(jù)包索引纳本。
Write a Function to Derive Playback Audio Queue Buffer Size - 編寫(xiě)函數(shù)獲取播放音頻隊(duì)列緩沖區(qū)的大小
音頻隊(duì)列服務(wù)期望您的應(yīng)用程序指定您使用的音頻隊(duì)列緩沖區(qū)的大小。 Listing 3-7
顯示了一種方法腋颠。 它產(chǎn)生足夠大的緩沖區(qū)大小來(lái)保存給定持續(xù)時(shí)間的音頻數(shù)據(jù)繁成。
在創(chuàng)建一個(gè)回放音頻隊(duì)列之后,您將在您的應(yīng)用程序中調(diào)用此DeriveBufferSize
函數(shù)淑玫,作為要求音頻隊(duì)列分配緩沖區(qū)的先決條件巾腕。 請(qǐng)參閱Set Sizes for a Playback Audio Queue。
與你在Write a Function to Derive Recording Audio Queue Buffer Size類(lèi)似的函數(shù)相比絮蒿,這里的代碼做了兩個(gè)額外的事情尊搬, 對(duì)于回放同樣如此:
- 每次您的回調(diào)調(diào)用
AudioFileReadPackets
函數(shù)時(shí),要讀取數(shù)據(jù)包的數(shù)量
- 每次您的回調(diào)調(diào)用
- 設(shè)置緩沖區(qū)大小的下限土涝,以避免過(guò)度頻繁的磁盤(pán)訪問(wèn)
這里的計(jì)算考慮了您從磁盤(pán)讀取的音頻數(shù)據(jù)格式佛寿。 格式包括可能影響緩沖區(qū)大小的所有因素,例如音頻通道的數(shù)量但壮。
// Listing 3-7 Deriving a playback audio queue buffer size
void DeriveBufferSize (
AudioStreamBasicDescription &ASBDesc, // 1
UInt32 maxPacketSize, // 2
Float64 seconds, // 3
UInt32 *outBufferSize, // 4
UInt32 *outNumPacketsToRead // 5
) {
static const int maxBufferSize = 0x50000; // 6
static const int minBufferSize = 0x4000; // 7
if (ASBDesc.mFramesPerPacket != 0) { // 8
Float64 numPacketsForTime =
ASBDesc.mSampleRate / ASBDesc.mFramesPerPacket * seconds;
*outBufferSize = numPacketsForTime * maxPacketSize;
} else { // 9
*outBufferSize =
maxBufferSize > maxPacketSize ?
maxBufferSize : maxPacketSize;
}
if ( // 10
*outBufferSize > maxBufferSize &&
*outBufferSize > maxPacketSize
)
*outBufferSize = maxBufferSize;
else { // 11
if (*outBufferSize < minBufferSize)
*outBufferSize = minBufferSize;
}
*outNumPacketsToRead = *outBufferSize / maxPacketSize; // 12
}
以下是這段代碼的工作原理:
- 音頻隊(duì)列的
AudioStreamBasicDescription
結(jié)構(gòu)冀泻。
- 音頻隊(duì)列的
- 您正在播放的音頻文件中數(shù)據(jù)的估計(jì)最大數(shù)據(jù)包大小。您可以通過(guò)調(diào)用
AudioFileGetProperty
函數(shù)(在AudioFile.h
頭文件中聲明)使用屬性IDkAudioFilePropertyPacketSizeUpperBound
來(lái)確定此值蜡饵。請(qǐng)參閱Set Sizes for a Playback Audio Queue弹渔。
- 您正在播放的音頻文件中數(shù)據(jù)的估計(jì)最大數(shù)據(jù)包大小。您可以通過(guò)調(diào)用
- 按照音頻的秒數(shù)指定每個(gè)音頻隊(duì)列緩沖區(qū)的大小。
- 輸出時(shí)溯祸,每個(gè)音頻隊(duì)列緩沖區(qū)的大兄ā(以字節(jié)為單位)。
- 輸出時(shí)您没,每次調(diào)用回放音頻隊(duì)列回調(diào)時(shí)從文件中讀取的音頻數(shù)據(jù)包的數(shù)量鸟召。
- 音頻隊(duì)列緩沖區(qū)大小的上限(以字節(jié)為單位)。在這個(gè)例子中氨鹏,上限設(shè)置為320 KB欧募。這對(duì)應(yīng)于大約五秒鐘的立體聲,采樣率為96kHz的24位音頻仆抵。
- 音頻隊(duì)列緩沖區(qū)大小的下限跟继,以字節(jié)為單位。在這個(gè)例子中镣丑,下限設(shè)置為16 KB舔糖。
- 對(duì)于定義每個(gè)數(shù)據(jù)包固定數(shù)量幀的音頻數(shù)據(jù)格式,獲取音頻隊(duì)列緩沖區(qū)大小莺匠。
- 對(duì)于沒(méi)有為每個(gè)數(shù)據(jù)包定義固定數(shù)量幀的音頻數(shù)據(jù)格式金吗,根據(jù)最大數(shù)據(jù)包大小和您設(shè)置的上限獲取合理的音頻隊(duì)列緩沖區(qū)大小。
- 如果導(dǎo)出的緩沖區(qū)大小高于您設(shè)置的上限,則根據(jù)估計(jì)的最大數(shù)據(jù)包大小調(diào)整邊界考慮摇庙。
- 如果導(dǎo)出的緩沖區(qū)大小低于您設(shè)置的下限旱物,則將其調(diào)整到界限。
- 計(jì)算每次調(diào)用回調(diào)時(shí)從音頻文件中讀取的數(shù)據(jù)包數(shù)量卫袒。
Open an Audio File for Playback - 打開(kāi)用于播放的音頻文件
現(xiàn)在您打開(kāi)一個(gè)音頻文件進(jìn)行播放宵呛,使用以下三個(gè)步驟:
- 獲取表示要播放的音頻文件的
CFURL
對(duì)象。
- 獲取表示要播放的音頻文件的
- 打開(kāi)文件夕凝。
- 獲取文件的音頻數(shù)據(jù)格式
1. Obtaining a CFURL Object for an Audio File - 從音頻文件中獲取CFURL對(duì)象
Listing 3-8
演示了如何獲取要播放的音頻文件的CFURL對(duì)象宝穗。 在下一步中使用CFURL對(duì)象,打開(kāi)文件码秉。
// Listing 3-8 Obtaining a CFURL object for an audio file
CFURLRef audioFileURL =
CFURLCreateFromFileSystemRepresentation ( // 1
NULL, // 2
(const UInt8 *) filePath, // 3
strlen (filePath), // 4
false // 5
);
以下是這段代碼的工作原理:
- 在
CFURL.h
頭文件中聲明的CFURLCreateFromFileSystemRepresentation
函數(shù)創(chuàng)建一個(gè)代表要播放文件的CFURL對(duì)象逮矛。
- 在
- 使用
NULL
(或kCFAllocatorDefault
)來(lái)使用當(dāng)前的默認(rèn)內(nèi)存分配器。
- 使用
- 您要轉(zhuǎn)換為CFURL對(duì)象的文件系統(tǒng)路徑转砖。 在生產(chǎn)代碼中橱鹏,通常會(huì)從用戶(hù)獲取
filePath
的值。
- 您要轉(zhuǎn)換為CFURL對(duì)象的文件系統(tǒng)路徑转砖。 在生產(chǎn)代碼中橱鹏,通常會(huì)從用戶(hù)獲取
- 文件系統(tǒng)路徑中的字節(jié)數(shù)堪藐。
- 值為
false
表示filePath
表示文件,而不是文件夾directory
挑围。
- 值為
2. Opening an Audio File - 打開(kāi)音頻文件
Listing 3-9
演示了如何打開(kāi)一個(gè)音頻文件進(jìn)行播放礁竞。
// Listing 3-9 Opening an audio file for playback
AQPlayerState aqData; // 1
OSStatus result =
AudioFileOpenURL ( // 2
audioFileURL, // 3
fsRdPerm, // 4
0, // 5
&aqData.mAudioFile // 6
);
CFRelease (audioFileURL); // 7
以下是這段代碼的工作原理:
- 創(chuàng)建
AQPlayerState
自定義結(jié)構(gòu)的實(shí)例(請(qǐng)參閱Define a Custom Structure to Manage State)。 當(dāng)您打開(kāi)音頻文件進(jìn)行播放時(shí)杉辙,您可以使用此實(shí)例作為放置表示音頻文件的音頻文件對(duì)象(AudioFileID
類(lèi)型)的位置模捂。
- 創(chuàng)建
- 在
AudioFile.h
頭文件中聲明的AudioFileOpenURL
函數(shù)打開(kāi)你想要播放的文件。
- 在
- 對(duì)要播放的文件的引用蜘矢。
- 您想要與您正在播放的文件一起使用的文件權(quán)限狂男。 可用權(quán)限在文件管理器的
File Access Permission Constants
枚舉中定義。 在這個(gè)例子中你要求讀取文件的權(quán)限品腹。
- 您想要與您正在播放的文件一起使用的文件權(quán)限狂男。 可用權(quán)限在文件管理器的
- 一個(gè)可選的文件類(lèi)型提示岖食。 此處的值為0表示該示例不使用此功能。
- 在輸出時(shí)舞吭,對(duì)音頻文件的引用被放置在自定義結(jié)構(gòu)的
mAudioFile
字段中泡垃。
- 在輸出時(shí)舞吭,對(duì)音頻文件的引用被放置在自定義結(jié)構(gòu)的
- 釋放在步驟1中創(chuàng)建的
CFURL
對(duì)象。
- 釋放在步驟1中創(chuàng)建的
3. Obtaining a File’s Audio Data Format - 獲取文件音頻數(shù)據(jù)格式
Listing 3-10
顯示了如何獲取文件的音頻數(shù)據(jù)格式
Listing 3-10 Obtaining a file’s audio data format
UInt32 dataFormatSize = sizeof (aqData.mDataFormat); // 1
AudioFileGetProperty ( // 2
aqData.mAudioFile, // 3
kAudioFilePropertyDataFormat, // 4
&dataFormatSize, // 5
&aqData.mDataFormat // 6
);
以下是這段代碼的工作原理:
- 獲取預(yù)期的屬性值大小羡鸥,用于查詢(xún)音頻文件的音頻數(shù)據(jù)格式蔑穴。
- 在
AudioFile.h
頭文件中聲明的AudioFileGetProperty
函數(shù)獲取音頻文件中指定屬性的值。
- 在
- 音頻文件對(duì)象(類(lèi)型為
AudioFileID
)惧浴,代表要獲取其音頻數(shù)據(jù)格式的文件存和。
- 音頻文件對(duì)象(類(lèi)型為
- 用于獲取音頻文件的數(shù)據(jù)格式的值的屬性ID。
- 輸入時(shí),描述音頻文件數(shù)據(jù)格式的
AudioStreamBasicDescription
結(jié)構(gòu)的預(yù)期大小捐腿。 在輸出上纵朋,實(shí)際的大小。 您的回放應(yīng)用程序不需要使用此值叙量。
- 輸入時(shí),描述音頻文件數(shù)據(jù)格式的
- 在輸出時(shí)倡蝙,以音頻文件的形式從
AudioStreamBasicDescription
結(jié)構(gòu)中獲得完整的音頻數(shù)據(jù)格式。 該行將文件的音頻數(shù)據(jù)格式存儲(chǔ)到音頻隊(duì)列的自定義結(jié)構(gòu)中绞佩,以將其應(yīng)用于音頻隊(duì)列嘴拢。
- 在輸出時(shí)倡蝙,以音頻文件的形式從
Create a Playback Audio Queue - 創(chuàng)建播放音頻隊(duì)列
Listing 3-11
顯示了如何創(chuàng)建一個(gè)回放音頻隊(duì)列。 請(qǐng)注意涂滴,AudioQueueNewOutput
函數(shù)使用前面步驟中配置的自定義結(jié)構(gòu)和回調(diào)街州,以及要播放的文件的音頻數(shù)據(jù)格式。
// Listing 3-11 Creating a playback audio queue
AudioQueueNewOutput ( // 1
&aqData.mDataFormat, // 2
HandleOutputBuffer, // 3
&aqData, // 4
CFRunLoopGetCurrent (), // 5
kCFRunLoopCommonModes, // 6
0, // 7
&aqData.mQueue // 8
);
以下是這段代碼的工作原理:
-
AudioQueueNewOutput
函數(shù)創(chuàng)建一個(gè)新的播放音頻隊(duì)列肘交。
-
- 音頻隊(duì)列正在設(shè)置播放的文件的音頻數(shù)據(jù)格式笆载。 請(qǐng)參閱 Obtaining a File’s Audio Data Format。
- 與回放音頻隊(duì)列一起使用的回調(diào)函數(shù)涯呻。 請(qǐng)參閱Write a Playback Audio Queue Callback凉驻。
- 播放音頻隊(duì)列的自定義數(shù)據(jù)結(jié)構(gòu)。 請(qǐng)參閱Define a Custom Structure to Manage State复罐。
- 當(dāng)前運(yùn)行循環(huán)涝登,以及將調(diào)用音頻隊(duì)列回放回調(diào)的循環(huán)。
- 可以調(diào)用回調(diào)的運(yùn)行循環(huán)模式效诅。 通常胀滚,在這里使用
kCFRunLoopCommonModes
常量。
- 可以調(diào)用回調(diào)的運(yùn)行循環(huán)模式效诅。 通常胀滚,在這里使用
- 保留乱投。 必須為0咽笼。
- 輸出時(shí),新分配的播放音頻隊(duì)列戚炫。
Set Sizes for a Playback Audio Queue - 設(shè)置回放音頻隊(duì)列的大小
接下來(lái)剑刑,您為播放音頻隊(duì)列設(shè)置一些大小。 在為音頻隊(duì)列分配緩沖區(qū)之前以及在開(kāi)始讀取音頻文件之前双肤,請(qǐng)使用這些大小叛甫。
本節(jié)中的代碼清單顯示如何設(shè)置:
- 音頻隊(duì)列緩沖區(qū)大小
- 每次調(diào)用回放音頻隊(duì)列回調(diào)時(shí)要讀取的數(shù)據(jù)包數(shù)量
- 數(shù)組大小,用于保存一個(gè)緩沖區(qū)的音頻數(shù)據(jù)的數(shù)據(jù)包描述
1. Setting Buffer Size and Number of Packets to Read - 設(shè)置緩存大小和要讀取的包的數(shù)量
Listing 3-12
演示了如何使用之前編寫(xiě)的DeriveBufferSize
函數(shù)(請(qǐng)參閱 Write a Function to Derive Playback Audio Queue Buffer Size)杨伙。 此處的目標(biāo)是為每個(gè)音頻隊(duì)列緩沖區(qū)設(shè)置一個(gè)以字節(jié)為單位的大小其监,并確定每次調(diào)用回放音頻隊(duì)列回調(diào)時(shí)要讀取的數(shù)據(jù)包數(shù)量。
此代碼使用保守估計(jì)的最大數(shù)據(jù)包大小限匣,Core Audio
通過(guò)kAudioFilePropertyPacketSizeUpperBound
屬性提供抖苦。 在大多數(shù)情況下毁菱,與比花費(fèi)時(shí)間讀取整個(gè)音頻文件來(lái)獲得實(shí)際的最大數(shù)據(jù)包大小相比,最好使用這種技術(shù) - 這是近似而快速的 锌历。
// Listing 3-12 Setting playback audio queue buffer size and number of packets to read
UInt32 maxPacketSize;
UInt32 propertySize = sizeof (maxPacketSize);
AudioFileGetProperty ( // 1
aqData.mAudioFile, // 2
kAudioFilePropertyPacketSizeUpperBound, // 3
&propertySize, // 4
&maxPacketSize // 5
);
DeriveBufferSize ( // 6
aqData.mDataFormat, // 7
maxPacketSize, // 8
0.5, // 9
&aqData.bufferByteSize, // 10
&aqData.mNumPacketsToRead // 11
);
以下是這段代碼的工作原理:
- 在
AudioFile.h
頭文件中聲明的AudioFileGetProperty
函數(shù)獲取音頻文件的指定屬性的值贮庞。在這里,你用它來(lái)獲得一個(gè)保守的上限究西,以字節(jié)為單位窗慎,你想播放的文件中的音頻數(shù)據(jù)包的大小。
- 在
- 表示要播放的文件的音頻文件對(duì)象(類(lèi)型為
AudioFileID
)卤材。請(qǐng)參閱打Opening an Audio File遮斥。
- 表示要播放的文件的音頻文件對(duì)象(類(lèi)型為
- 用于獲取音頻文件中數(shù)據(jù)包大小保守上限的屬性ID。
- 在輸出上扇丛,
kAudioFilePropertyPacketSizeUpperBound
屬性的大惺趼稹(以字節(jié)為單位)。
- 在輸出上扇丛,
- 在輸出上帆精,對(duì)于要播放的文件较屿,數(shù)據(jù)包大小的保守上限(以字節(jié)為單位)。
- 在Write a Function to Derive Playback Audio Queue Buffer Size中描述的
DeriveBufferSize
函數(shù)設(shè)置每次調(diào)用回放音頻隊(duì)列回調(diào)時(shí)要讀取的緩沖區(qū)大小和數(shù)據(jù)包的數(shù)量卓练。
- 在Write a Function to Derive Playback Audio Queue Buffer Size中描述的
- 您要播放的文件的音頻數(shù)據(jù)格式隘蝎。請(qǐng)參閱Obtaining a File’s Audio Data Format。
- 音頻文件中估計(jì)的最大數(shù)據(jù)包大小襟企,來(lái)自此列表的第5行末贾。
- 每個(gè)音頻隊(duì)列緩沖區(qū)應(yīng)該容納的音頻的秒數(shù)。半秒鐘整吆,如這里設(shè)置,通常是一個(gè)不錯(cuò)的選擇辉川。
- 輸出時(shí)表蝙,每個(gè)音頻隊(duì)列緩沖區(qū)的大小(以字節(jié)為單位)乓旗。該值放置在音頻隊(duì)列的自定義結(jié)構(gòu)中府蛇。
- 輸出時(shí),每次調(diào)用播放音頻隊(duì)列回調(diào)時(shí)要讀取的數(shù)據(jù)包數(shù)量屿愚。該值也放置在音頻隊(duì)列的自定義結(jié)構(gòu)中汇跨。
2. Allocating Memory for a Packet Descriptions Array - 為包的描述數(shù)組分配內(nèi)存
現(xiàn)在,您為數(shù)組分配內(nèi)存妆距,以保存一個(gè)緩沖區(qū)的音頻數(shù)據(jù)的數(shù)據(jù)包描述穷遂。 恒定比特率數(shù)據(jù)不使用數(shù)據(jù)包描述,因此Listing 3-13
中的CBR情況步驟3非常簡(jiǎn)單娱据。
// Listing 3-13 Allocating memory for a packet descriptions array
bool isFormatVBR = ( // 1
aqData.mDataFormat.mBytesPerPacket == 0 ||
aqData.mDataFormat.mFramesPerPacket == 0
);
if (isFormatVBR) { // 2
aqData.mPacketDescs =
(AudioStreamPacketDescription*) malloc (
aqData.mNumPacketsToRead * sizeof (AudioStreamPacketDescription)
);
} else { // 3
aqData.mPacketDescs = NULL;
}
以下是這段代碼的工作原理:
- 確定音頻文件的數(shù)據(jù)格式是VBR還是CBR蚪黑。 在VBR數(shù)據(jù)中,每個(gè)字節(jié)數(shù)據(jù)包或幀每個(gè)數(shù)據(jù)包值中的一個(gè)或兩個(gè)是可變的,因此在音頻隊(duì)列的
AudioStreamBasicDescription
結(jié)構(gòu)中將被列為0忌穿。
- 確定音頻文件的數(shù)據(jù)格式是VBR還是CBR蚪黑。 在VBR數(shù)據(jù)中,每個(gè)字節(jié)數(shù)據(jù)包或幀每個(gè)數(shù)據(jù)包值中的一個(gè)或兩個(gè)是可變的,因此在音頻隊(duì)列的
- 對(duì)于包含VBR數(shù)據(jù)的音頻文件抒寂,為數(shù)據(jù)包描述數(shù)組分配內(nèi)存。 根據(jù)每次調(diào)用回放回調(diào)時(shí)要讀取的音頻數(shù)據(jù)包的數(shù)量來(lái)計(jì)算所需的內(nèi)存掠剑。 請(qǐng)參閱Setting Buffer Size and Number of Packets to Read屈芜。
- 對(duì)于包含CBR數(shù)據(jù)(如線(xiàn)性PCM)的音頻文件,音頻隊(duì)列不使用數(shù)據(jù)包描述數(shù)組朴译。
Set a Magic Cookie for a Playback Audio Queue - 為播放音頻隊(duì)列設(shè)置Magic Cookie
某些壓縮音頻格式(如MPEG 4 AAC
)利用結(jié)構(gòu)來(lái)包含音頻元數(shù)據(jù)井佑。 這些結(jié)構(gòu)被稱(chēng)為Magic Cookie
。 當(dāng)您使用音頻隊(duì)列服務(wù)以這種格式播放文件時(shí)动分,您將從音頻文件中獲取Magic Cookie毅糟,并在開(kāi)始播放之前將其添加到音頻隊(duì)列中。
Listing 3-14
展示了如何從一個(gè)文件中獲得一個(gè)Magic Cookie并將其應(yīng)用到一個(gè)音頻隊(duì)列中澜公。 開(kāi)始播放之前姆另,您的代碼會(huì)調(diào)用此函數(shù)。
// Listing 3-14 Setting a magic cookie for a playback audio queue
UInt32 cookieSize = sizeof (UInt32); // 1
bool couldNotGetProperty = // 2
AudioFileGetPropertyInfo ( // 3
aqData.mAudioFile, // 4
kAudioFilePropertyMagicCookieData, // 5
&cookieSize, // 6
NULL // 7
);
if (!couldNotGetProperty && cookieSize) { // 8
char* magicCookie =
(char *) malloc (cookieSize);
AudioFileGetProperty ( // 9
aqData.mAudioFile, // 10
kAudioFilePropertyMagicCookieData, // 11
&cookieSize, // 12
magicCookie // 13
);
AudioQueueSetProperty ( // 14
aqData.mQueue, // 15
kAudioQueueProperty_MagicCookie, // 16
magicCookie, // 17
cookieSize // 18
);
free (magicCookie); // 19
}
以下是這段代碼的工作原理:
- 為magic cookie數(shù)據(jù)設(shè)置估計(jì)大小坟乾。
- 捕獲
AudioFileGetPropertyInfo
函數(shù)的結(jié)果迹辐。如果成功,則此函數(shù)返回NoErr
的值甚侣,相當(dāng)于布爾值false明吩。
- 捕獲
- 在
AudioFile.h
頭文件中聲明的AudioFileGetPropertyInfo
函數(shù)獲取指定屬性值的大小。您可以使用它來(lái)設(shè)置保存屬性值的變量的大小殷费。
- 在
- 音頻文件對(duì)象(類(lèi)型為
AudioFileID
)印荔,表示要播放的音頻文件。
- 音頻文件對(duì)象(類(lèi)型為
- 屬性ID代表音頻文件的magic cookie數(shù)據(jù)详羡。
- 在輸入時(shí)仍律,magic cookie數(shù)據(jù)的估計(jì)大小。在輸出上实柠,實(shí)際的大小水泉。
- 使用
NULL
來(lái)表示您不關(guān)心屬性的讀/寫(xiě)訪問(wèn)權(quán)限。
- 使用
- 如果音頻文件包含一個(gè)magic cookie窒盐,分配內(nèi)存來(lái)持有它草则。
- 在
AudioFile.h
頭文件中聲明的AudioFileGetProperty
函數(shù)獲取指定屬性的值。在這種情況下蟹漓,它會(huì)獲取音頻文件的magic cookie炕横。
- 在
- 音頻文件對(duì)象(類(lèi)型為
AudioFileID
),表示您要播放的音頻文件葡粒,以及您獲取的magic cookie看锉。
- 音頻文件對(duì)象(類(lèi)型為
- 表示音頻文件magic cookie數(shù)據(jù)的屬性ID姿锭。
- 在輸入上,使用
AudioFileGetPropertyInfo
函數(shù)獲得的magicCookie
變量的大小伯铣。在輸出上呻此,根據(jù)寫(xiě)入到magicCookie
變量的字節(jié)數(shù)來(lái)計(jì)算magic cookie的實(shí)際大小。
- 在輸入上,使用
- 輸出時(shí)腔寡,音頻文件的magic cookie焚鲜。
-
AudioQueueSetProperty
函數(shù)在音頻隊(duì)列中設(shè)置一個(gè)屬性。在這種情況下放前,它為音頻隊(duì)列設(shè)置一個(gè)magic cookie忿磅,匹配要播放的音頻文件中的magic cookie。
-
- 您要為其設(shè)置magic cookie的音頻隊(duì)列凭语。
- 屬性ID代表音頻隊(duì)列的magic cookie葱她。
- 您要播放的音頻文件中的magic cookie。
- magic cookie的大小似扔,以字節(jié)為單位吨些。
- 釋放為magic cookie分配的內(nèi)存。
Allocate and Prime Audio Queue Buffers - 分配和填充音頻隊(duì)列緩沖區(qū)
現(xiàn)在您可以詢(xún)問(wèn)您創(chuàng)建的音頻隊(duì)列(在Create a Playback Audio Queue中)以準(zhǔn)備一組音頻隊(duì)列緩沖區(qū)炒辉。 Listing 3-15
演示了如何做到這一點(diǎn)豪墅。
// Listing 3-15 Allocating and priming audio queue buffers for playback
aqData.mCurrentPacket = 0; // 1
for (int i = 0; i < kNumberBuffers; ++i) { // 2
AudioQueueAllocateBuffer ( // 3
aqData.mQueue, // 4
aqData.bufferByteSize, // 5
&aqData.mBuffers[i] // 6
);
HandleOutputBuffer ( // 7
&aqData, // 8
aqData.mQueue, // 9
aqData.mBuffers[i] // 10
);
}
以下是這段代碼的工作原理:
- 將數(shù)據(jù)包索引設(shè)置為0,以便當(dāng)音頻隊(duì)列回調(diào)開(kāi)始填充緩沖區(qū)時(shí)(步驟7)黔寇,它將從音頻文件的開(kāi)始處開(kāi)始偶器。
- 分配和填充一組音頻隊(duì)列緩沖區(qū)。 (您可以在Define a Custom Structure to Manage State缝裤。將此編號(hào)
kNumberBuffers
設(shè)置為3)屏轰。
- 分配和填充一組音頻隊(duì)列緩沖區(qū)。 (您可以在Define a Custom Structure to Manage State缝裤。將此編號(hào)
-
AudioQueueAllocateBuffer
函數(shù)通過(guò)為其分配內(nèi)存來(lái)創(chuàng)建音頻隊(duì)列緩沖區(qū)。
-
- 正在分配音頻隊(duì)列緩沖區(qū)的音頻隊(duì)列憋飞。
- 新的音頻隊(duì)列緩沖區(qū)的大小(以字節(jié)為單位)搀崭。
- 在輸出上,將新的音頻隊(duì)列緩沖區(qū)添加到自定義結(jié)構(gòu)中的
mBuffers
數(shù)組中猾编。
- 在輸出上,將新的音頻隊(duì)列緩沖區(qū)添加到自定義結(jié)構(gòu)中的
-
HandleOutputBuffer
函數(shù)是你寫(xiě)的回放音頻隊(duì)列回調(diào)函數(shù)瘤睹。 請(qǐng)參閱 Write a Playback Audio Queue Callback。
-
- 音頻隊(duì)列的自定義結(jié)構(gòu)答倡。
- 您正在調(diào)用的回調(diào)的音頻隊(duì)列轰传。
- 您傳遞給音頻隊(duì)列回調(diào)的音頻隊(duì)列緩沖區(qū)。
Set an Audio Queue’s Playback Gain - 設(shè)置音頻隊(duì)列的播放增益
在您告訴音頻隊(duì)列開(kāi)始播放之前瘪撇,您需要通過(guò)音頻隊(duì)列參數(shù)機(jī)制設(shè)置其增益获茬。 Listing 3-16
顯示了如何做到這一點(diǎn)港庄。 有關(guān)參數(shù)機(jī)制的更多信息,請(qǐng)參閱Audio Queue Parameters恕曲。
// Listing 3-16 Setting an audio queue’s playback gain
Float32 gain = 1.0; // 1
// Optionally, allow user to override gain setting here
AudioQueueSetParameter ( // 2
aqData.mQueue, // 3
kAudioQueueParam_Volume, // 4
gain // 5
);
以下是這段代碼的工作原理:
- 在0(用于靜音)和1(用于單位增益)之間設(shè)置與音頻隊(duì)列一起使用的增益鹏氧。
-
AudioQueueSetParameter
函數(shù)設(shè)置音頻隊(duì)列的參數(shù)值。
-
- 您正在設(shè)置參數(shù)的音頻隊(duì)列佩谣。
- 您正在設(shè)置的參數(shù)的ID把还。
kAudioQueueParam_Volume
常量讓您設(shè)置音頻隊(duì)列的增益。
- 您正在設(shè)置的參數(shù)的ID把还。
- 您正在應(yīng)用到音頻隊(duì)列的增益設(shè)置茸俭。
Start and Run an Audio Queue - 開(kāi)始和運(yùn)行音頻隊(duì)列
所有前面的代碼都說(shuō)明了播放文件的過(guò)程吊履。 這包括啟動(dòng)音頻隊(duì)列并在文件播放時(shí)保持運(yùn)行循環(huán),如Listing 3-17
所示
// Listing 3-17 Starting and running an audio queue
aqData.mIsRunning = true; // 1
AudioQueueStart ( // 2
aqData.mQueue, // 3
NULL // 4
);
do { // 5
CFRunLoopRunInMode ( // 6
kCFRunLoopDefaultMode, // 7
0.25, // 8
false // 9
);
} while (aqData.mIsRunning);
CFRunLoopRunInMode ( // 10
kCFRunLoopDefaultMode,
1,
false
);
以下是這段代碼的工作原理:
- 在自定義結(jié)構(gòu)中設(shè)置一個(gè)標(biāo)志來(lái)指示音頻隊(duì)列正在運(yùn)行调鬓。
-
AudioQueueStart
函數(shù)在其自己的線(xiàn)程上啟動(dòng)音頻隊(duì)列艇炎。
-
- 音頻隊(duì)列開(kāi)始。
- 使用
NULL
來(lái)指示音頻隊(duì)列應(yīng)該立即開(kāi)始播放腾窝。
- 使用
- 定期輪詢(xún)自定義結(jié)構(gòu)的
mIsRunning
字段缀踪,以檢查音頻隊(duì)列是否已停止。
- 定期輪詢(xún)自定義結(jié)構(gòu)的
-
CFRunLoopRunInMode
函數(shù)運(yùn)行包含音頻隊(duì)列線(xiàn)程的運(yùn)行循環(huán)燕锥。
-
- 使用運(yùn)行循環(huán)的默認(rèn)模式辜贵。
- 將運(yùn)行循環(huán)的運(yùn)行時(shí)間設(shè)置為0.25秒。
- 使用false來(lái)指示運(yùn)行循環(huán)應(yīng)該持續(xù)指定的全部時(shí)間归形。
- 音頻隊(duì)列停止后托慨,再運(yùn)行一次運(yùn)行循環(huán),以確保當(dāng)前正在播放的音頻隊(duì)列緩沖區(qū)有時(shí)間完成暇榴。
Clean Up After Playing - 播放完后的清理
當(dāng)您完成播放文件時(shí)厚棵,請(qǐng)釋放音頻隊(duì)列,關(guān)閉音頻文件并釋放剩余的資源蔼紧。 Listing 3-18
說(shuō)明了這些步驟婆硬。
// Listing 3-18 Cleaning up after playing an audio file
AudioQueueDispose ( // 1
aqData.mQueue, // 2
true // 3
);
AudioFileClose (aqData.mAudioFile); // 4
free (aqData.mPacketDescs); // 5
以下是這段代碼的工作原理:
-
AudioQueueDispose
函數(shù)銷(xiāo)毀音頻隊(duì)列及其所有資源,包括其緩沖區(qū)奸例。
-
- 您想要處理的音頻隊(duì)列彬犯。
- 使用true來(lái)同步銷(xiāo)毀音頻隊(duì)列。
- 關(guān)閉播放的音頻文件查吊。
AudioFileClose
函數(shù)在AudioFile.h
頭文件中聲明谐区。
- 關(guān)閉播放的音頻文件查吊。
- 釋放用來(lái)保存數(shù)據(jù)包描述的內(nèi)存。
后記
未完逻卖,待續(xù)~~~