Android NDK sample 之 RhythmGame

Reference


简介

此示例演示了如何构建一个简单的音乐游戏。
游戏是先听拍手声,然后在一段时间内按照听到的节奏点击屏幕以复制这些拍手声。

启动页


视频演示

  • 黄色:游戏正在加载(assets 正在解压)
  • 灰色:正在玩游戏
  • 橙色:按早了
  • 绿色:刚刚好
  • 紫色:按晚了
  • 红色:加载游戏时出现问题(检查 logcat 输出)

Audio timeline

游戏在小节的前 3 个节拍上播放拍手声。这些与背景音轨一起播放。
当用户点击屏幕时,会播放拍手声,游戏会检查点击是否发生在可接受的时间窗口内。

Architecture

Latency optimizations

  • 性能模式设置为Low Latency低延迟
  • 分享模式设置为Exclusive独占
  • 缓冲区大小设置为突发中帧数的两倍(双缓冲)

Audio rendering

IRenderableAudio 接口(抽象类)表示可以生成音频数据帧的对象。 PlayerMixer 对象都实现了这个接口。
拍手声和背景音轨都由“Player”对象表示,然后使用“Mixer”将它们混合在一起。

Sharing objects with the audio thread

音频线程(调用 onAudioReady 方法)永远不会被阻塞,这一点非常重要。阻塞会导致欠载和音频故障。为了避免阻塞,我们使用“LockFreeQueue”在音频线程和其他线程之间共享信息。

下图显示了如何通过将拍手时间(以毫秒为单位)推送到队列中,然后在播放拍手时将拍手时间出列来将拍手排入队列。

我们还使用 atomics 来确保线程看到任何共享原语的视图一致性。

Keeping UI events and audio in sync

当点击事件到达 UI 线程时,它只包含事件发生的时间(自启动以来的毫秒数)。我们需要弄清楚点击发生时歌曲的位置。

为此,我们会跟踪歌曲位置和上次更新时间。每次调用 onAudioReady 方法时都会更新这些值。 这使我们能够使 UI 与音频时间线保持同步。

Calculating whether a tap was successful

一旦我们知道用户何时点击歌曲,我们就可以计算该点击是否成功,即是否在可接受的时间范围内。这个范围被称为“点击窗口”。

一旦我们知道点击的结果,UI 就会更新颜色,为用户提供视觉反馈。这是在getTapResult中完成的。

请注意,一旦接收到点击,点击窗口就会从队列中删除 - 用户只有一次机会正确点击!

Use of compressed audio assets

为了减少 APK 大小,该游戏使用 MP3 文件作为其音频 assets。这些是在游戏启动时在 AAssetDataSource::newFromCompressedAsset 中提取的。在此过程中将显示黄色屏幕。

默认情况下,游戏使用 NDKExtractor 进行 assets 提取和解码。在幕后,它使用 NDK Media API

这种方法有一些限制:

  • 仅适用于 API 21 及以上。
  • 无重采样:提取的输出格式将匹配 MP3 的输入格式。在这种情况下,采样率为 48000。如果您的音频流的采样率不匹配,则不会提取 assets,并且 logcat 中将显示错误。
  • 仅 16 位输出。

更快、更通用的解决方案是使用 FFmpeg。为此,请按照 此处的说明 并使用在“app.gradle”中找到的“ffmpegExtractor”构建变体。然后提取将由FFmpegExtractor完成。


Oboe 基础

Oboe 是一个 C++ 库,可以轻松地在 Android 上构建高性能音频应用程序。应用程序通过向流读取和写入数据来与 Oboe 通信。

Audio streams

Oboe 在您的应用程序与 Android 设备上的音频输入和输出之间移动音频数据。您的应用程序使用回调函数或通过读取和写入音频流来传入和传出数据,由类 AudioStream 表示。读/写调用可以是阻塞的或非阻塞的。

流由以下定义:

  • audio device作为流中数据的源或接收器。
  • sharing mode确定流是否对音频设备具有独占访问权限,否则该音频设备可能会在多个流之间共享。
  • format是流中的音频数据。

Audio device

每个流都附加到单个音频设备。

音频设备是硬件接口或虚拟端点,充当连续数字音频数据流的源或接收器。不要将音频设备(内置麦克风或蓝牙耳机)与运行您的应用的 Android 设备(手机或手表)混淆。

在 API 23 及更高版本上,您可以使用 AudioManager 方法 getDevices() 来发现 Android 设备上可用的音频设备。该方法返回有关每个设备类型的信息。

每个音频设备在 Android 设备上都有一个唯一的 ID。您可以使用 ID 将音频流绑定到特定的音频设备。但是,在大多数情况下,您可以让 Oboe 选择默认的主要设备,而不是自己指定一个。

连接到流的音频设备确定流是用于输入还是输出。一个流只能在一个方向上移动数据。当你定义一个流时,你也设置了它的方向。当您打开一个流时,Android 会检查以确保音频设备和流方向一致。

Sharing mode

流具有共享模式:

  • SharingMode::Exclusive独家的(在 API 26+ 上可用):意味着流可以独占访问其音频设备上的端点;端点不能被任何其他音频流使用。如果独占端点已在使用中,则流可能无法访问它。独占流通过绕过混合器阶段提供尽可能低的延迟,但它们也更有可能断开连接。您应该在不再需要它们时立即关闭独占流,以便其他应用程序可以访问该端点。并非所有音频设备都提供独占端点。系统声音和来自其他应用程序的声音在使用独占流时仍然可以听到,因为它们使用不同的端点。
  • SharingMode::Shared共享的:允许 Oboe 流共享一个端点。操作系统将混合分配给音频设备上同一端点的所有共享流。

您可以在创建流时明确请求共享模式,但不能保证您会收到该模式。默认情况下,共享模式为 Shared。

Audio format

通过流传递的数据具有通常的数字音频属性,您必须在定义流时指定这些属性。这些如下:

  • Sample format
  • Samples per frame
  • Sample rate

Oboe 允许这些 Sample format:

AudioFormat C data type Notes
I16 int16_t common 16-bit samples, Q0.15 format
Float float -1.0 to +1.0

Oboe 可能会自行执行采样转换。例如,如果应用程序正在写入 AudioFormat::Float 数据,但 HAL 使用 AudioFormat::I16,则 Oboe 可能会自动转换采样。转换可以发生在任一方向。如果您的应用程序处理音频输入,那么验证输入格式并准备好在必要时转换数据是明智的,如下例所示:

1
2
3
4
5
AudioFormat dataFormat = stream->getDataFormat();
//... later
if (dataFormat == AudioFormat::I16) {
convertFloatToPcm16(...)
}

Creating an audio stream

Oboe 库遵循构建器设计模式并提供类 AudioStreamBuilder。

Set the audio stream configuration using an AudioStreamBuilder.

使用与流参数对应的构建器函数。这些可选的设置功能可用:

1
2
3
4
5
6
7
8
9
AudioStreamBuilder streamBuilder;
streamBuilder.setDeviceId(deviceId);
streamBuilder.setDirection(direction);
streamBuilder.setSharingMode(shareMode);
streamBuilder.setSampleRate(sampleRate);
streamBuilder.setChannelCount(channelCount);
streamBuilder.setFormat(format);
streamBuilder.setPerformanceMode(perfMode);

请注意,这些方法不会报告错误,例如未定义的常量或值超出范围。当流打开时,它们将被检查。

如果不指定 deviceId,则默认为主要输出设备。如果不指定流方向,则默认为输出流。对于所有参数,您可以明确设置一个值,或者通过根本不指定参数或将其设置为 kUnspecified 来让系统分配最佳值。

为安全起见,请在创建音频流后检查其状态,如下面的第 3 步所述。

Open the Stream

配置 AudioStreamBuilder 后,调用 openStream() 打开流:

1
2
3
4
Result result = streamBuilder.openStream(&stream_);
if (result != OK){
__android_log_print(ANDROID_LOG_ERROR, "AudioEngine", "Error opening stream %s", convertToText(result));
}

Verifying stream configuration and additional properties

您应该在打开流后验证它的配置。

保证设置以下属性。但是,如果未指定这些属性,则仍将设置默认值,并应由适当的访问器查询。

  • framesPerCallback
  • sampleRate
  • channelCount
  • format
  • direction

即使显式设置以下属性也可能由底层流构造更改,因此应始终由适当的访问器查询。属性设置将取决于设备功能。

  • bufferCapacityInFrames
  • sharingMode (exclusive provides lowest latency)
  • performanceMode

以下属性仅由底层流设置。它们不能由应用程序设置,但应由适当的访问器查询。

  • framesPerBurst

以下属性具有异常行为

  • deviceId 的值,当底层 API 为 AAudio(API 级别 >=28)时才会生效,当使用 OpenSLES 时设置是无效的。当使用 OpenSLES 时,无论如何设置 deviceId 都不会抛出错误,并且会使用默认设备,而不是指定的任何设备。
  • mAudioApi 只是构建器的一个属性,但是 AudioStream::getAudioApi() 可用于查询流使用的底层 API。构建器中设置的属性不能保证,一般情况下,API 应该由 Oboe 选择,以考虑最佳性能和稳定性。由于 Oboe 设计为在两个 API 之间尽可能统一,因此通常不需要此属性。
  • mBufferSizeInFrames 只能在已经打开的流上设置(与构建器相反),因为它取决于运行时行为。实际使用的大小可能不是所要求的大小。Oboe 或底层 API 将限制零和缓冲区容量之间的大小。还可以进一步限制以减少特定设备上的毛刺。将回调与 OpenSL ES 结合使用时,不支持此功能。

许多流的属性可能会有所不同(无论您是否设置它们),具体取决于音频设备的功能和运行它的 Android 设备。如果您需要知道这些值,则必须在打开流后使用访问器查询它们。此外,授予流的基础参数有助于了解它们是否未指定。作为一个好的防御性编程问题,您应该在使用之前检查流的配置。

有一些函数可以检索与每个构建器设置对应的流设置:

AudioStreamBuilder set methods AudioStream get methods
setDataCallback() getDataCallback()
setErrorCallback() getErrorCallback()
setDirection() getDirection()
setSharingMode() getSharingMode()
setPerformanceMode() getPerformanceMode()
setSampleRate() getSampleRate()
setChannelCount() getChannelCount()
setFormat() getFormat()
setBufferCapacityInFrames() getBufferCapacityInFrames()
setFramesPerCallback() getFramesPerCallback()
- getFramesPerBurst()
setDeviceId() (not respected on OpenSLES) getDeviceId()
setAudioApi() (mainly for debugging) getAudioApi()

API 28 中添加了以下 AudioStreamBuilder 字段,以指定有关设备的 AudioStream 的其他信息。目前,它们对流的影响很小,但设置它们有助于应用程序更好地与其他服务交互。

有关更多信息,请参阅:Usage/ContentTypes。 设备可以使用 InputPreset 来处理输入流(例如增益控制)。默认情况下,它设置为 VoiceRecognition,它针对低延迟进行了优化。

  • setUsage(oboe::Usage usage) - 创建流的目的。
  • setContentType(oboe::ContentType contentType) - 流承载的内容类型。
  • setInputPreset(oboe::InputPreset inputPreset) - 音频输入的录音配置。
  • setSessionId(SessionId sessionId) - 分配 SessionID 以连接到 Java AudioEffects API。

Using an audio stream

State transitions

Oboe 流通常处于五种稳定状态之一(错误状态,断开连接,在本节末尾描述):

  • Open
  • Started
  • Paused
  • Flushed
  • Stopped

数据仅在流处于已启动状态时流过流。要在状态之间移动流,请使用请求状态转换的函数之一:

1
2
3
4
5
Result result;
result = stream->requestStart();
result = stream->requestStop();
result = stream->requestPause();
result = stream->requestFlush();

请注意,您只能在输出流上请求暂停或刷新:

这些函数是异步的,状态更改不会立即发生。当您请求状态更改时,流将移动到相应的瞬态之一:

  • Starting
  • Pausing
  • Flushing
  • Stopping
  • Closing

下面的状态图将稳定状态显示为圆角矩形,将瞬态显示为虚线矩形。尽管未显示,但您可以从任何状态调用 close()

Oboe 不提供回调来提醒您状态变化。一种特殊函数 AudioStream::waitForStateChange() 可用于等待状态更改。请注意,大多数应用程序不需要调用 waitForStateChange() 并且可以在需要时请求状态更改。

该函数不会自行检测状态变化,也不会等待特定状态。它一直等到当前状态与您指定的 inputState 不同。

例如,在请求暂停后,流应该立即进入暂时状态 Pausing,并在稍后到达 Paused 状态 - 尽管不能保证它会。由于不能等待 Paused 状态,因此使用 waitForStateChange() 等待除 Pausing 之外的任何状态。这是如何做到的:

1
2
3
4
5
StreamState inputState = StreamState::Pausing;
StreamState nextState = StreamState::Uninitialized;
int64_t timeoutNanos = 100 * kNanosPerMillisecond;
result = stream->requestPause();
result = stream->waitForStateChange(inputState, &nextState, timeoutNanos);

如果流的状态不是 Pausing(输入状态,我们假设它是调用时的当前状态),函数立即返回。否则,它会阻塞,直到状态不再是 Pausing 或超时到期。当函数返回时,参数 nextState 显示流的当前状态。

您可以在调用 request start、stop 或 flush 之后使用相同的技术,使用相应的瞬态作为 inputState。不要在调用 AudioStream::close() 后调用 waitForStateChange(),因为一旦关闭,底层流资源将被删除。并且不要在另一个线程中运行 waitForStateChange() 时调用 close()。

Reading and writing to an audio stream

有两种方法可以将数据移入或移出流。

  1. 从流中读取或直接写入流。
  2. 指定一个数据回调对象,当流准备好时会被调用。

回调技术提供最低的延迟性能,因为回调代码可以在高优先级线程中运行。此外,尝试在没有音频回调的情况下打开低延迟输出流(意图使用写入)可能会导致非低延迟流。

当您不需要低延迟时,读/写技术可能更容易。或者,当同时进行输入和输出时,通常使用回调进行输出,然后从输入流中进行非阻塞读取。然后,您可以在一个高优先级线程中获得输入和输出数据。

流启动后,您可以使用 AudioStream::read(buffer, numFrames, timeoutNanos) 和 AudioStream::write(buffer, numFrames, timeoutNanos) 方法对其进行读取或写入。

对于传输指定帧数的阻塞读取或写入,请将 timeoutNanos 设置为大于零。对于非阻塞调用,将 timeoutNanos 设置为零。在这种情况下,结果是实际传输的帧数。

当您读取输入时,您应该验证读取的帧数是否正确。否则,缓冲区可能包含可能导致音频故障的未知数据。您可以用零填充缓冲区以创建静默丢失:

1
2
3
4
5
6
7
8
Result result = stream.read(audioData, numFrames, timeout);
if (result < 0) {
// Error!
}
if (result != numFrames) {
// pad the buffer with zeros
memset(static_cast<sample_type*>(audioData) + result * samplesPerFrame, 0, (numFrames - result) * stream.getBytesPerFrame());
}

您可以在启动流之前通过向其中写入数据或静默来填充流的缓冲区。这必须在 timeoutNanos 设置为零的非阻塞调用中完成。

缓冲区中的数据必须与 stream.getDataFormat() 返回的数据格式匹配。

Closing an audio stream

使用完流后,将其关闭:

1
stream->close();

不要在流被另一个线程写入或读取时关闭它,因为这会导致您的应用程序崩溃。关闭流后,除了查询其属性外,不应调用其任何方法。

Disconnected audio stream

如果发生以下事件之一,音频流可能随时断开连接:

  • 不再连接关联的音频设备(例如,拔下耳机时)。
  • 内部发生错误。
  • 音频设备不再是主要的音频设备。

当流断开连接时,它具有”Disconnected”状态并且调用 write() 或其他函数将返回 Result::ErrorDisconnected。当流断开连接时,您所能做的就是关闭它。

如果您需要在音频设备断开连接时收到通知,请编写一个扩展 AudioStreamErrorCallback 的类,然后使用 builder.setErrorCallback(yourCallbackClass) 注册您的类。如果您注册一个回调,那么如果流断开连接,它将在单独的线程中自动关闭流。

您的回调可以实现以下方法(在单独的线程中调用):

  • onErrorBeforeClose(stream, error) - 当流已断开但尚未关闭时调用,因此您仍然可以引用底层流(例如getXRunCount())。您还可以通知可能正在调用流的任何其他线程停止这样做。不要在此回调中删除流或修改其流状态。
  • onErrorAfterClose(stream, error) - 当流被 Oboe 停止并关闭时调用,因此无法使用流并且调用 getState() 将返回closed。在此回调期间,可以查询流属性(构建器请求的属性)以及写入和读取的帧。可以在此方法结束时删除流(只要它没有在其他线程中引用)。不应调用引用底层流的方法(例如 getTimestamp()、getXRunCount()、read()、write() 等)。打开一个单独的流也是这个回调的合理使用,特别是如果收到的错误是 Error::Disconnected。但是,重要的是要注意,新的音频设备可能与断开连接的流具有截然不同的属性。

Optimizing performance

您可以通过使用特殊的高优先级线程来优化音频应用程序的性能。

Using a high priority data callback

如果您的应用程序从普通线程读取或写入音频数据,则可能会被抢占或遇到时序抖动。这可能会导致音频故障。使用较大的缓冲区可能会防止出现此类故障,但较大的缓冲区也会引入更长的音频延迟。对于需要低延迟的应用程序,音频流可以使用异步回调函数与您的应用程序传输数据。回调在具有更好性能的高优先级线程中运行。

您的代码可以通过实现虚拟类 AudioStreamDataCallback 来访问回调机制。该流定期执行 onAudioReady()(回调函数)以获取其下一次突发的数据。

您需要填充的采样总数为 numFrames * numChannels。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class AudioEngine : AudioStreamDataCallback {
public:
DataCallbackResult AudioEngine::onAudioReady(AudioStream *oboeStream, void *audioData, int32_t numFrames){
// Fill the output buffer with random white noise.
const int numChannels = AAudioStream_getChannelCount(stream);
// This code assumes the format is AAUDIO_FORMAT_PCM_FLOAT.
float *output = (float *)audioData;
for (int frameIndex = 0; frameIndex < numFrames; frameIndex++) {
for (int channelIndex = 0; channelIndex < numChannels; channelIndex++) {
float noise = (float)(drand48() - 0.5);
*output++ = noise;
}
}
return DataCallbackResult::Continue;
}
bool AudioEngine::start() {
...
// register the callback
streamBuilder.setDataCallback(this);
}
private:
// application data goes here
}

请注意,回调必须使用 setDataCallback 在流上注册。任何特定于应用程序的数据都可以包含在类本身中。

回调函数不应对调用它的流执行读取或写入操作。如果回调属于输入流,则您的代码应处理 audioData 缓冲区中提供的数据(指定为第二个参数)。如果回调属于输出流,则您的代码应将数据放入缓冲区。

在回调中可以处理多个流。您可以使用一个流作为主数据流,并在类的私有数据中将指针传递给其他流。为主流注册回调。然后在其他流上使用非阻塞 I/O。这是将输入流传递到输出流的往返回调的示例。主调用流是输出流。输入流包含在类中。

回调从输入流中进行非阻塞读取,将数据放入输出流的缓冲区中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class AudioEngine : AudioStreamDataCallback {
public:
DataCallbackResult AudioEngine::onAudioReady(AudioStream *oboeStream, void *audioData, int32_t numFrames) {
const int64_t timeoutNanos = 0; // for a non-blocking read
auto result = recordingStream->read(audioData, numFrames, timeoutNanos);
// result has type ResultWithValue<int32_t>, which for convenience is coerced to a Result type when compared with another Result.
if (result == Result::OK) {
if (result.value() < numFrames) {
// replace the missing data with silence
memset(static_cast<sample_type*>(audioData) + result.value() * samplesPerFrame, 0, (numFrames - result.value()) * oboeStream->getBytesPerFrame());
}
return DataCallbackResult::Continue;
}
return DataCallbackResult::Stop;
}
bool AudioEngine::start() {
...
streamBuilder.setDataCallback(this);
}
void setRecordingStream(AudioStream *stream) {
recordingStream = stream;
}
private:
AudioStream *recordingStream;
}

请注意,在此示例中,假设输入和输出流具有相同数量的通道、格式和采样率。流的格式可能不匹配 - 只要代码正确处理翻译。

Data Callback - Do’s and Don’ts

您永远不应该执行可能在 onAudioReady 内部阻塞的操作。阻塞操作的例子包括:

  • 使用例如 malloc() 或 new 分配内存
  • 文件操作,例如打开、关闭、读取或写入
  • 网络操作,如流媒体
  • 使用互斥锁或其他同步原语
  • sleep
  • 停止或关闭流
  • 在调用它的流上调用 read() 或 write()

但是以下方法都可以调用:

  • AudioStream::get*()
  • oboe::convertResultToText()

Setting performance mode

每个 AudioStream 都有一个性能模式,它对你的应用程序的行为有很大的影响。 共有三种模式:

  • PerformanceMode::None是默认模式。它使用平衡延迟和节能的基本流。
  • PerformanceMode::LowLatency使用较小的缓冲区和优化的数据路径来减少延迟。
  • PerformanceMode::PowerSaving使用更大的内部缓冲区和数据路径,以牺牲延迟换取低功耗。

您可以通过调用 setPerformanceMode() 选择性能模式,并通过调用 getPerformanceMode() 发现当前模式。

如果应用程序中低延迟比节能更重要,请使用 PerformanceMode::LowLatency。这对于交互性很强的应用程序非常有用,例如游戏或键盘合成器。

如果在您的应用程序中节能比低延迟更重要,请使用 PerformanceMode::PowerSaving。这对于播放以前生成的音乐的应用程序来说很典型,例如流式音频或 MIDI 文件播放器。

在当前版本的 Oboe 中,为了实现尽可能低的延迟,您必须使用 PerformanceMode::LowLatency 性能模式以及高优先级数据回调。 按照这个例子:

1
2
3
4
5
6
7
8
9
10
11
// Create a callback object
MyOboeStreamCallback myCallback;
// Create a stream builder
AudioStreamBuilder builder;
builder.setDataCallback(myCallback);
builder.setPerformanceMode(PerformanceMode::LowLatency);
// Use it to create the stream
AudioStream *stream;
builder.openStream(&stream);

Thread safety

Oboe API 不是完全线程安全的。您不能一次从多个线程同时调用某些 Oboe 函数。这是因为 Oboe 避免使用互斥体,这会导致线程抢占和故障。

为安全起见,不要调用 waitForStateChange() 或从两个不同的线程读取或写入同一流。同样,不要在另一个线程中读取或写入流时关闭一个线程中的流。

返回流设置的调用,如 AudioStream::getSampleRate() 和 AudioStream::getChannelCount(),是线程安全的。

这些调用也是线程安全的:

  • convertToText()
  • AudioStream::get*(),除了 getTimestamp() 和 getState()

注意:当流使用错误回调时,从回调线程读/写是安全的,同时也从运行它的线程关闭流。

API

oboe::AudioStreamBase

包含音频流和构建器参数的基类。

Public

返回值 函数 描述 评论
  AudioStreamBase (const AudioStreamBase &)=default 默认复制构造函数
AudioStreamBase & operator= (const AudioStreamBase &)=default 默认赋值运算符
int32_t getChannelCount () const 返回通道数 例如 2 表示立体声,或 kUnspecified
Direction getDirection () const 返回Direction::Input或Direction::Output
int32_t getSampleRate () const 返回流的采样率或 kUnspecified
int32_t getFramesPerDataCallback () const 返回每个数据回调或 kUnspecified 中的帧数。
AudioFormat getFormat () const 返回音频采样格式(例如 Float 或 I16)
virtual int32_t getBufferSizeInFrames () 查询不阻塞可以填充的最大帧数。 如果流已关闭,则将返回最后一个已知值。
virtual int32_t getBufferCapacityInFrames () const 返回 capacityInFrames 或 kUnspecified
SharingMode getSharingMode () const 返回流的共享模式。
PerformanceMode getPerformanceMode () const 返回流的性能模式。
int32_t getDeviceId () const 返回流的设备 ID。
AudioStreamDataCallback * getDataCallback () const 如果设置,则返回此流的数据回调对象。 仅限内部使用。
AudioStreamErrorCallback * getErrorCallback () const 如果设置,则返回此流的错误回调对象。 仅限内部使用。
bool isDataCallbackSpecified () const 如果为此流设置了数据回调,则返回 true
bool isErrorCallbackSpecified () const 如果为此流设置了错误回调,则返回 true 请注意,如果应用程序未设置错误回调,则可能会提供默认回调。
Usage getUsage () const 返回此流的使用情况。
ContentType getContentType () const 返回流的内容类型。
InputPreset getInputPreset () const 返回流的输入预设。
SessionId getSessionId () const 返回流的会话 ID 分配策略(无或分配)。
bool isChannelConversionAllowed () const 如果 Oboe 可以转换通道计数以获得最佳结果,则返回 true。
bool isFormatConversionAllowed () const 如果 Oboe 可以转换数据格式以获得最佳结果,则返回 true。
SampleRateConversionQuality getSampleRateConversionQuality () const 返回 Oboe 是否以及如何转换采样率以获得最佳结果。

Protected

返回值 函数 描述
virtual Result isValidConfig () 验证在较低层中可能未检查的流参数

oboe::AudioStream

Oboe C++ 音频流的基类。

Public

返回值 函数 描述 评论
  AudioStream (const AudioStreamBuilder &builder) 使用给定的 AudioStreamBuilder 构造一个 AudioStream
virtual Result open () 根据当前设置打开一个流。 请注意,我们不建议重新打开已关闭的流。 TODO 我们应该阻止重新开放吗?
virtual Result close () 关闭流并从 open() 调用中释放任何资源。
virtual Result start (int64_t timeoutNanoseconds=kDefaultTimeoutNanos) 启动流。 这将阻塞,直到流已启动、发生错误或已达到 timeoutNanoseconds。
virtual Result pause (int64_t timeoutNanoseconds=kDefaultTimeoutNanos) 暂停流。 这将阻塞,直到流暂停、发生错误或达到 timeoutNanoseconds。
virtual Result flush (int64_t timeoutNanoseconds=kDefaultTimeoutNanos) 刷新流。 这将阻塞,直到流被刷新、发生错误或达到 timeoutNanoseconds。
virtual Result stop (int64_t timeoutNanoseconds=kDefaultTimeoutNanos) 停止流。 这将阻塞,直到流停止、发生错误或达到 timeoutNanoseconds。
virtual Result requestStart ()=0 异步启动流。 立即返回(不阻塞)。相当于调用start(0)。
virtual Result requestPause ()=0 异步暂停流。 立即返回(不阻塞)。相当于调用pause(0)。
virtual Result requestFlush ()=0 异步刷新流。 立即返回(不阻塞)。相当于调用flush(0)。
virtual Result requestStop ()=0 异步停止流。 立即返回(不阻塞)。相当于调用 stop(0)。
virtual StreamState getState () const =0 查询当前状态,例如StreamState::Pausing
virtual Result waitForStateChange (StreamState inputState, StreamState *nextState, int64_t timeoutNanoseconds)=0 等到流的当前状态不再与输入状态匹配。传递输入状态是为了避免由调用之间的状态变化引起的竞争条件。 请注意,通常应用程序不需要调用它。它被认为是一种先进的技术,主要用于测试。
如果状态在超时期限内没有改变,那么它将返回 ErrorTimeout。即使 timeoutNanoseconds 为零也是如此。
virtual ResultWithValue< int32_t > setBufferSizeInFrames (int32_t) 这可用于通过更改将发生阻塞的阈值来调整缓冲区的延迟。 通过将其与 getXRunCount() 相结合,可以在运行时为每个设备调整延迟。
这不能设置为高于 getBufferCapacity()。
virtual ResultWithValue< int32_t > getXRunCount () const XRun 是欠载或超载。在播放过程中,如果没有及时写入流,并且系统没有有效数据,就会发生欠载。在记录过程中,如果没有及时读取流,并且没有地方放置传入的数据,则会发生溢出,因此将其丢弃。 欠载或超载会导致可听见的“爆裂声”或“故障”。
virtual bool isXRunCountSupported () const =0 如果流支持 XRun 计数,则返回 true
virtual int32_t getFramesPerBurst ()=0 查询端点一次读写的帧数。
int32_t getBytesPerFrame () const 获取每个音频帧中的字节数。 这是使用通道计数和样本格式计算的。例如,一个 2 通道浮点流每帧将有 2 * 4 = 8 个字节。
int32_t getBytesPerSample () const 获取每个样本的字节数。 这是使用示例格式计算的。例如,使用 16 位整数样本的流每个样本将有 2 个字节。
virtual int64_t getFramesWritten () 写入流的音频帧数。 这个单调计数器永远不会被重置。
virtual int64_t getFramesRead () 从流中读取的音频帧数。 这个单调计数器永远不会被重置。
virtual ResultWithValue< double > calculateLatencyMillis () 基于 getTimestamp() 计算流的延迟。 输出延迟是给定帧从应用程序传输到某种类型的数模转换器所需的时间。如果 DAC 是外部的,例如在 USB 接口或通过 HDMI 连接的电视中,则可能存在 Android 设备不知道的额外延迟。
输入延迟是给定帧从模数转换器 (ADC) 传输到应用程序所需的时间。
请注意,当您向 OUTPUT 流写入数据时,它的延迟会突然增加,然后随着数据的消耗而缓慢减少。
当您从中读取数据时,INPUT 流的延迟会突然减少,然后随着更多数据的到达而缓慢增加。
OUTPUT 流的延迟通常高于 INPUT 延迟,因为应用程序通常会尝试保持 OUTPUT 缓冲区已满而 INPUT 缓冲区为空。
virtual ResultWithValue< FrameTimestamp > getTimestamp (clockid_t) 获取 framePosition 处的帧进入或离开音频处理管道的估计时间。 这可用于协调事件和与外部环境的交互,并估计音频流的延迟。
virtual ResultWithValue< int32_t > write (const void *, int32_t, int64_t) 将提供的缓冲区中的数据写入流中。 此方法将阻塞,直到写入完成或超时。
如果 timeoutNanoseconds 为零,则此调用将不会等待。
virtual ResultWithValue< int32_t > read (void *, int32_t, int64_t) 从流中将数据读入提供的缓冲区。 此方法将阻塞,直到读取完成或超时。
如果 timeoutNanoseconds 为零,则此调用将不会等待。
virtual AudioApi getAudioApi () const =0 获取流使用的底层音频 API。
bool usesAAudio () const 如果底层音频 API 是 Audio,则返回 true。
void launchStopThread () 启动一个线程用于停止流
virtual void updateFramesWritten ()=0 更新 mFramesWritten。 仅限内部使用。
virtual void updateFramesRead ()=0 更新 mFramesRead。 仅限内部使用。
AudioStreamDataCallback * swapDataCallback (AudioStreamDataCallback *dataCallback)
AudioStreamErrorCallback * swapErrorCallback (AudioStreamErrorCallback *errorCallback)
ResultWithValue< int32_t > getAvailableFrames () 返回当前缓冲区中的数据帧数
ResultWithValue< int32_t > waitForAvailableFrames (int32_t numFrames, int64_t timeoutNanoseconds) 等到流的缓冲区中有最少量的可用数据。 这可以与 EXCLUSIVE MMAP 输入流一起使用,以避免读取数据太靠近 DSP 写入位置,这可能会导致故障。
virtual oboe::Result getLastErrorCallbackResult () const 返回从错误回调传递的最后一个结果

Protected

返回值 函数 描述 评论
bool wasErrorCallbackCalled () 这用于从流中检测多个错误回调。 在某些 Android 版本中有一些bug会触发多次错误回调。
调用它会设置 atomic 为真并返回前一个值。
virtual Result waitForStateTransition (StreamState startingState, StreamState endingState, int64_t timeoutNanoseconds) 等待从一种状态转换到另一种状态。
virtual DataCallbackResult onDefaultCallback (void *, int) 覆盖它以提供应用程序未指定回调时的默认值。
DataCallbackResult fireDataCallback (void *audioData, int numFrames) 覆盖它以提供您自己的音频回调行为
bool isDataCallbackEnabled () 如果可以调用回调,则返回 true
void setDataCallbackEnabled (bool enabled) 这可以在内部设置为 false 以防止在返回 DataCallbackResult::Stop 后回调。
void setWeakThis (std::shared_ptr< oboe::AudioStream > &sharedStream)
std::shared_ptr< oboe::AudioStream > lockWeakThis ()

oboe::AudioStreamBuilder

音频流的工厂类。

Public

返回值 函数 描述 评论
  AudioStreamBuilder (const AudioStreamBase &audioStreamBase)
AudioStreamBuilder * setChannelCount (int channelCount) 请求特定数量的频道。 默认值为 kUnspecified。如果未指定值,则应用程序应在打开流后查询实际值。
AudioStreamBuilder * setDirection (Direction direction) 请求流的方向。 默认值为 Direction::Output。
AudioStreamBuilder * setSampleRate (int32_t sampleRate) 请求以 Hz 为单位的特定采样率。 默认值为 kUnspecified。如果未指定值,则应用程序应在打开流后查询实际值。
从技术上讲,这应该称为“帧速率”或“每秒帧数”,因为它指的是每秒传输的完整帧数。但它传统上称为“采样率”。
AudioStreamBuilder * setFramesPerDataCallback (int framesPerCallback) 为数据回调请求特定数量的帧。 默认值为 kUnspecified。如果未指定该值,则实际数量可能因回调而异。如果应用程序可以处理不同数量的帧,那么我们建议不要指定它。这允许底层 API 优化回调。但是,例如,如果您的应用程序执行 FFT 或其他面向块的操作,则调用此函数以获得所需的大小。
AudioStreamBuilder * setFormat (AudioFormat format) 请求示例数据格式,例如 Format::Float。 默认为格式::未指定。如果未指定值,则应用程序应在打开流后查询实际值。
AudioStreamBuilder * setBufferCapacityInFrames (int32_t bufferCapacityInFrames) 以帧为单位设置请求的缓冲区容量。BufferCapacityInFrames 是最大可能的 BufferSizeInFrames。 最终的流容量可能不同。对于 AAudio,它至少应该有这么大。对于 OpenSL ES,它可以更小。
默认值为 kUnspecified。
AudioApi getAudioApi () const 获取打开流时将请求的音频 API。 不能保证这是实际使用的 API。查询流本身以找出正在使用的 API。
如果不指定 API,则在 isAAudioRecommended() 返回 true 时将使用 AAudio。否则将使用 OpenSL ES。
AudioStreamBuilder * setAudioApi (AudioApi audioApi) 如果您未指定此项,则 Oboe 将在运行时为设备和 SDK 版本选择最佳 API。 这应该几乎总是未指定,除了调试目的。
指定AAudio会强制 Oboe 在8.0上使用AAudio,风险极大。
指定 OpenSLES 应主要用于测试传统性能/功能。
如果调用方请求 AAudio 并且支持它,则将使用 AAudio。
AudioStreamBuilder * setSharingMode (SharingMode sharingMode) 请求共享设备的模式。 请求的共享模式可能不可用。 所以应用程序应该在流打开后查询实际模式。
AudioStreamBuilder * setPerformanceMode (PerformanceMode performanceMode) 请求流的性能级别。 这将决定延迟、功耗和故障保护级别。
AudioStreamBuilder * setUsage (Usage usage) 设置输出流的预期用例。 系统将使用此信息来优化流的行为。例如,这可能会影响处理流的音量和焦点的方式。输入流的用法被忽略。如果不调用此函数,默认值为 Usage::Media。在 API 级别 28 中添加。
AudioStreamBuilder * setContentType (ContentType contentType) 设置输出流将携带的音频数据的类型。 系统将使用此信息来优化流的行为。例如,这可能会影响通知发生时流是否暂停。输入流的 contentType 将被忽略。
如果不调用此函数,默认值为 ContentType::Music。
在 API 级别 28 中添加。
AudioStreamBuilder * setInputPreset (InputPreset inputPreset) 设置流的输入(捕获)预设。 系统将使用此信息来优化流的行为。例如,这可能会影响使用哪些麦克风以及如何处理记录的数据。如果不调用此函数,默认值为 InputPreset::VoiceRecognition。这是因为 VoiceRecognition 是许多平台上延迟最低的预设。在 API 级别 28 中添加。
AudioStreamBuilder * setSessionId (SessionId sessionId) 设置请求的会话 ID。 会话 ID 可用于将流与效果处理器相关联,使用 Android AudioEffect Java API 控制效果。
如果不调用此函数,默认值为 SessionId::None。如果设置为 SessionId::Allocate,则在打开流时将分配会话 ID。分配的会话 ID 可以通过调用 AudioStream::getSessionId() 获得,然后在打开另一个流时与此函数一起使用。 这允许在流之间共享效果。
来自 Oboe 的会话 ID 可用于 Android Java API,反之亦然。因此,可以将 Oboe 流中的会话 ID 传递给 Java,并使用 Java AudioEffect API 应用效果。分配的会话 ID 将始终为正且非零。
在 API 级别 28 中添加。
AudioStreamBuilder * setDeviceId (int32_t deviceId) 在给定音频设备 ID 的情况下,向特定音频输入/输出设备请求流。 在大多数情况下,主要设备将是要使用的适当设备,并且可以将 deviceId 保留为 kUnspecified。
例如,在 Android 上,可以从 Java AudioManager 获取 ID。AudioManager.getDevices() 返回一个 AudioDeviceInfo[] 数组,其中包含一个 getId() 方法(以及其他类型信息),应该传递给这个方法。
请注意,当使用 OpenSL ES 时,这将被忽略并且创建的流将具有 deviceId kUnspecified。
AudioStreamBuilder * setDataCallback (oboe::AudioStreamDataCallback *dataCallback) 指定一个对象来处理来自底层 API 的数据相关回调。
AudioStreamBuilder * setErrorCallback (oboe::AudioStreamErrorCallback *errorCallback) 指定一个对象来处理来自底层 API 的错误相关回调。
当由于耳机插入或拔出而导致流断开时,可能会发生这种情况。
如果音频服务失败或独占流被另一个流窃取,也会发生这种情况。
当发生错误回调时,必须在单独的线程中停止并关闭关联的流。
AudioStreamBuilder * setCallback (AudioStreamCallback *streamCallback) 指定一个对象来处理来自底层 API 的数据或与错误相关的回调。 这相当于调用 setDataCallback() 和 setErrorCallback()。
当发生错误回调时,关联的流将在单独的线程中停止并关闭。
AudioStreamBuilder * setChannelConversionAllowed (bool allowed) 如果为真,则 Oboe 可能会转换通道数以达到最佳效果。例如,在某些版本的 Android 上,立体声流无法使用 FAST 轨道。因此可能会使用单声道流并复制到两个通道。在某些设备上,单声道流可能会中断,因此可能会打开立体声流并将其转换为单声道。
默认为真。
AudioStreamBuilder * setFormatConversionAllowed (bool allowed) 如果为真,则 Oboe 可能会转换数据格式以获得最佳结果。 例如,在某些版本的 Android 上,浮动流无法获得低延迟数据路径。因此,可能会打开 I16 流并将其转换为浮点数。
默认为假。
AudioStreamBuilder * setSampleRateConversionQuality (SampleRateConversionQuality quality) 在 Oboe 中指定采样率转换器的质量。 如果设置为 None 则 Oboe 不会进行采样率转换。但如果您指定采样率,底层 API 可能仍会进行采样率转换。这可能会阻止您获得低延迟流。如果您在 Oboe 中进行转换,那么您可能仍会获得低延迟流。默认为 SampleRateConversionQuality::None
bool willUseAAudio () const 如果将根据当前设置使用 AAudio,则返回 true。
Result openStream (std::shared_ptr< oboe::AudioStream > &stream) 根据当前设置创建并打开流对象。 调用者共享指向 AudioStream 对象的指针。 shared_ptr 由 Oboe 内部使用,以防止流在回调使用时被删除。
Result openManagedStream (ManagedStream &stream) 根据当前构建器状态创建并打开 ManagedStream 对象。 调用者必须创建一个唯一的 ptr,并通过引用传递,以便可以修改它以指向一个打开的流。调用者拥有唯一的 ptr,超出范围会自动关闭删除。

Static Public

返回值 函数 描述 评论
static bool isAAudioSupported () 此设备是否支持 AAudio API? Oreo 8.0 版本中引入了 AAudio。
static bool isAAudioRecommended () 推荐这款设备使用 AAudio API 吗? 由于版本特定问题,可能支持 AAudio 但不推荐使用。Android 8.0 或更早版本不建议使用 AAudio。

oboe::AudioStreamDataCallback

AudioStreamDataCallback 定义了一个回调接口,用于使用 onAudioReady 将数据移入/移出音频流使用 onError* 方法在流出现错误时发出警报。
它与 AudioStreamBuilder::setDataCallback() 一起使用。

Public

返回值 函数 描述 评论
virtual DataCallbackResult onAudioReady (AudioStream audioStream, void audioData, int32_t numFrames)=0 缓冲区已准备好进行处理。 对于输出流,此函数应以流的当前数据格式呈现和写入 numFrames 数据到 audioData 缓冲区。
对于输入流,此函数应从 audioData 缓冲区读取和处理 numFrames 数据。
音频数据通过缓冲区传递。所以不要在进行回调的流上调用 read() 或 write() 。
请注意,除非调用 AudioStreamBuilder::setFramesPerCallback(),否则 numFrames 可能会有所不同。
另请注意,此回调函数应被视为“实时”函数。它不能做任何可能导致无限延迟的事情,因为这可能导致音频出现故障或爆裂。
如果您需要移动数据,例如。 MIDI 命令,传入或传出回调函数,然后我们建议使用非阻塞技术,例如原子 FIFO。

oboe::AudioStreamErrorCallback

AudioStreamErrorCallback 定义了一个回调接口,用于在流出现错误或使用 onError* 方法断开连接时收到警报。
它与 AudioStreamBuilder::setErrorCallback() 一起使用。

Public

返回值 函数 描述 评论
virtual bool onError (AudioStream *, Result) 当流发生错误时,例如当流断开连接时,这将在其他 onError 方法之前调用。 它可用于覆盖和自定义正常的错误处理。使用这种方法被认为是一种先进的技术。例如,如果应用程序想要在关闭和重新打开流时使用高级锁,则可以使用它。或者,当应用程序想要向处理所有流状态的管理线程发送信号时,可能会使用它。
如果此方法返回 false,则表明流尚未被应用程序停止和关闭。在这种情况下,Oboe 将通过以下方式停止它:将调用 onErrorBeforeClose(),然后关闭流并关闭 onErrorAfterClose()。
如果此方法返回 true,则表示应用程序已停止并关闭流,并且 Oboe 不会这样做。在这种情况下,应用程序必须 stop() 和 close() 流。
该方法将在 Oboe 创建的线程上调用。
virtual void onErrorBeforeClose (AudioStream *, Result) 当流上发生错误时,例如当流断开连接时,并且如果 onError() 返回 false(表示尚未处理错误)时,将调用此方法。 请注意,这将在 Oboe 创建的线程上调用。
底层流将在 Oboe 已停止但尚未关闭时调用。所以可以查询流。
不要在此方法中关闭或删除流,因为此方法返回后它将被关闭。
virtual void onErrorAfterClose (AudioStream *, Result) 当流上发生错误时,例如当流断开连接时,并且如果 onError() 返回 false(表示尚未处理错误)时,将调用此方法。 底层 AAudio 或 OpenSL ES 流已被 Oboe 停止和关闭。因此无法引用底层流。但是您仍然可以查询大多数参数。此回调可用于在另一台设备上重新打开新流。

oboe::StabilizedCallback

Public

返回值 函数 描述 评论
  StabilizedCallback (AudioStreamCallback *callback)
DataCallbackResult onAudioReady (AudioStream oboeStream, void audioData, int32_t numFrames) override 缓冲区已准备好进行处理。 Implements oboe::AudioStreamDataCallback.
void onErrorBeforeClose (AudioStream *oboeStream, Result error) override 当流上发生错误时,例如当流断开连接时,并且如果 onError() 返回 false(表示尚未处理错误)时,将调用此方法。 Reimplemented from oboe::AudioStreamErrorCallback.
void onErrorAfterClose (AudioStream *oboeStream, Result error) override 当流上发生错误时,例如当流断开连接时,并且如果 onError() 返回 false(表示尚未处理错误)时,将调用此方法。 Reimplemented from oboe::AudioStreamErrorCallback.

oboe::DefaultStreamValues

在 API 16 到 26 上,将使用 OpenSL ES。使用 OpenSL ES 时,本地代码不知道 sampleRate 和 framesPerBurst 的最佳值。在 API 17+ 上,应使用以下代码从 AudioManager 获取这些值:

1
2
3
4
5
6
// Note that this technique only works for built-in speakers and headphones.
AudioManager myAudioMgr = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
String sampleRateStr = myAudioMgr.getProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE);
int defaultSampleRate = Integer.parseInt(sampleRateStr);
String framesPerBurstStr = myAudioMgr.getProperty(AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER);
int defaultFramesPerBurst = Integer.parseInt(framesPerBurstStr);

然后它可以通过 JNI 传递给 Oboe。

AAudio 会从 HAL 获取最佳的 framesPerBurst 并忽略此值。

oboe::ResultWithValue< T >

ResultWithValue 可以存储操作的结果(OK 或错误)和值。

它是为调用者需要知道操作是否成功,以及如果成功则需要在操作期间获得的值的情况而设计的。

例如,当从流中读取时,调用者需要知道读取操作的结果,如果成功,读取了多少帧。请注意,ResultWithValue 可以作为布尔值进行评估,因此检查结果是否正常很简单。

1
2
3
4
5
6
ResultWithValue<int32_t> resultOfRead = myStream.read(&buffer, numFrames, timeoutNanoseconds);
if (resultOfRead) {
LOGD("Frames read: %d", resultOfRead.value());
} else {
LOGD("Error reading from stream: %s", resultOfRead.error());
}

Public

返回值 函数 描述 评论
  ResultWithValue (oboe::Result error) 构造一个包含错误结果的 ResultWithValue。
  ResultWithValue (T value) 构造一个包含 OK 结果和值的 ResultWithValue。
oboe::Result error () const 得到结果。
T value () const 获取值
  operator bool () const 如果正常则返回 true
bool operator ! () const 检查错误的快速方法。
  operator Result () const 隐式转换为结果。 这可以轻松地与结果值进行比较。

Static Public

返回值 函数 描述 评论
static ResultWithValue< T > createBasedOnSign (T numericResult) 从一个数字创建一个 ResultWithValue。 如果数字为正数,则 ResultWithValue 的结果为 Result::OK 并且该值将包含该数字。如果数字是负数,则结果将从负数中获得(数字错误代码可以在 AAudio.h 中找到)并且该值将为空。

oboe::LatencyTuner

LatencyTuner 可用于动态调整输出流的延迟。它通过监控欠载次数来调整流的 bufferSize。

这只会影响与最接近应用程序的第一级缓冲相关的延迟。它不会影响 HAL 中的低延迟或 UI 中的触摸延迟。

如果使用回调,请在从数据回调函数返回之前立即调用 tune()。如果使用阻塞写入,请在调用 write() 之前调用 tune()。

如果您想查看此调整过程的持续结果,请定期调用 stream->getBufferSize()。

Public

返回值 函数 描述 评论
  LatencyTuner (AudioStream &stream) 构造一个新的 LatencyTuner 对象,它将作用于给定的音频流
  LatencyTuner (AudioStream &stream, int32_t maximumBufferSize) 构造一个新的 LatencyTuner 对象,它将作用于给定的音频流 可指定最大值
Result tune () 调整 bufferSizeInFrames 以优化延迟。 它将以低延迟开始,然后在发生欠载时提高它。仅 AAudio 支持延迟调整。
void requestReset () 这可以从另一个线程调用。然后 tune() 将调用 reset(),这会将延迟降低到最低限度,然后在出现故障时允许它回升。 这通常是为了响应用户决定最小化延迟而调用的。换句话说,从按钮处理程序调用它。
bool isAtMaximumBufferSize () 如果音频流的缓冲区大小为最大值,则返回 true。 如果在构造 LatencyTuner 时未指定最大值,则使用 stream->getBufferCapacityInFrames 的值
void setMinimumBufferSize (int32_t bufferSize) 设置调谐器重置时使用的最小缓冲区大小(以帧为单位)。 您可能希望在调用它之后调用 request Reset()。
int32_t getMinimumBufferSize () const
void setBufferSizeIncrement (int32_t sizeIncrement) 设置调整时缓冲区大小将增加的数量。默认情况下,这将是一次突发。 请注意,AAudio 会将缓冲区大小量化为 burstSize 的倍数。所以最终的缓冲区大小可能不是这个增量的倍数。
int32_t getBufferSizeIncrement () const

oboe::OboeGlobals

Static Public

返回值 函数 描述
static bool areWorkaroundsEnabled ()
static void setWorkaroundsEnabled (bool enabled) 在编写测试重现 AAudio 或 OpenSL ES 中的错误时禁用此功能,这些错误在 Oboe 中具有变通方法。

代码

RhythmGame:集成了编译好的 FFmpeg 的 so 文件。


分析


本文标题:Android NDK sample 之 RhythmGame

文章作者:魏超

发布时间:2021年08月26日 - 12:08

最后更新:2021年09月05日 - 19:09

原始链接:http://www.weichao.io/2021/08/26/Android-NDK-sample-之-RhythmGame/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

---------------------本文结束---------------------