[Android Things] I2S

Android Things Preview 6中已移除本文中提及的重要API,并变更了I2S音频的实现方式,新的I2S音频实现方式请关注本萌接下来的文章。
被移除的I2S API为:

  • AudioInputDriver
  • AudioOutputDriver
  • I2sDevice

Inter-IC Sound (I2S) - 集成音频总线,用于连接数字音频设备并发送和接受PCM音频数据。它可以作为PCM音频信号输出,也可以作为PCM音频信号输入,也可以同时作为PCM音频信号的输出和输入。

所以说,我们可以用I2S总线来做数字音频的播放和录制。那么问题来了,有的设备(比如说树莓派)本身就有耳机插孔来播放声音了,还要单独做这个数字音频的播放干什么?为了装逼烧钱穷骨头发烧么?~→_→

本萌来给你解答~= ̄ω ̄=:

  1. 发烧是肯定有的,但不一定是在穷骨头上
  2. 首先要了解数字音频和模拟音频在播放效果上的区别
    • 模拟音频是直接声音的变化转换为电压或者电流的强度变化输出出来,因为输出的电压电流幅度不大(和设备中其他信号的幅度差别不大),所以很容易受到其他信号的影响,混入干扰噪声,等到模拟音频信号被放大出来播放时,噪音也被一起放大。最常见的一种信号噪声乱入就是,把拨打中的电话拿到音响旁,音响发出的滴滴滴...滴滴滴...的声音
    • 数字音频发送出来的信号永远是0/1高低电平,高电平加上噪音信号还是高电平,不会有影响,而且数字信号是可以很方便的实现纠错的,所以抗干扰能力和纠错能力都比模拟音频信号强很多。数字音频信号送到外部的播放器上解码为模拟音频信号并放大时,播放器会有针对性的对这一微小的区域做干扰屏蔽,所以等到模拟音频信号已经被放大后,干扰噪声信号和它比起来已经微小到不易察觉了
  3. 在像树莓派这样的设备上,它的模拟音频输出是靠GPIO端口来模拟的,而且还和模拟视频信号走同一个端口,而且还有很多主板上的干扰信号没有被屏蔽,所以播放出来的声音就像收音机里出来的声音一样,死啦死啦的当然树莓派是可以从HDMI端口输出音频信号的,但HDMI音频分离器也死贵死贵的,不是土豪都肉疼所以通过I2S输出音频信号是个不过的选择

实现方法


连接硬件

下面这个端口引脚图就是树莓派2和3的引脚示意图:


[Android Things] I2S_第1张图片
树莓派I2S引脚端口

你不色盲的话,应该可以轻易发现I2S的4个引脚,当然你色盲但眼睛不瞎的话,也能从引脚旁的文字找到刚刚说的那4个I2S引脚:

  • BCLK - Pin12,位时钟(Bit Clock),由I2S Master Device(I2S主设备)产生,也称为SCLK,对应数字音频的每一位数据,SCLK都有1个脉冲,BCLK的频率 = 2 × 采样频率 × 采样位数
  • LRCLK - Pin35,左右声道选择时钟,LRCK的频率等于采样频率,它输出低电平时,SDOUT和SDIN的数据都为左声道数据,它输出高电平时,SDIN和SDIN的数据都为右声道数据,也称为FS(Frame Select)或WS(Word Select)
  • SDIN - Pin38,为PCM数据输入,外部从设备的PCM信号输入到主设备上录制或处理等
  • SDOUT - Pin40,为PCM数据输出,把主设备的音频信号输出到外部从设备去播放或录制等
  • GND - Pin6/9/14/20/25/30/34/39,公共接地,为主从设备提供低电平参考,随便接哪一个都行

I2S引脚和其他I2S设备的接线图如下,如果你只需要音频输入或者只需要音频输出,直接少接SDOUT或SDIN就好了。另外输入和输出都共用BCLK时钟线和LRCLK时钟线,所以主时钟由I2S Master Device(I2S主设备)产生就好了。


[Android Things] I2S_第2张图片
I2S总线接线图

树莓派引脚配置

树莓派上的PWM、I2S和模拟音频输出(用PWM来模拟的音频信号)都使用同一个时钟源,而树莓派默认是使用PWM功能的,所以你在使用树莓派的I2S功能之前需要在Boot配置中把PWM引脚的功能切换为I2S,即从PWM模式切换为音频模式。
这里需要了解树莓派的这两种配置模式:

  • PWM模式时,I2S和模拟音频是被禁用的,BCM18这个引脚是作为名为PWM0的PWM设备使用的
  • 音频模式时,I2S和模拟音频可以使用,BCM18这个引脚则作为I2S总线中的BCLK(位时钟)使用

按照以下步骤来切换树莓派的这这两种模式:

  1. 用读卡器把烧写了最新Android Things镜像(至少为Android Things Developer Preview4.1及其以上)的TF卡插入PC上
  2. 计算机中打开RPIBOOT分区。如果你的PC是Linux或者MAC,请自行mount分区后再操作。具体步骤:配置音频模式
  3. 打开里面的config.txt文件,并修改里面的dtoverlay属性的值:
    • 配置为PWM模式:dtoverlay=pwm-2chan-with-clk,pin=18,func=2,pin2=13,func2=4
    • 配置为音频模式:dtoverlay=generic-i2s
  4. 把修改完毕的TF插入树莓派,然后启动树莓派

管理I2S连接

1. 列举I2S设备
在Android Things中管理外围设备永远都是使用PeripheralManagerService,I2S也是外围设备之一,它使用到的引脚也都基于GPIO,所以PeripheralManagerService是跑不了的
这里列举出来的其实都是设备的名称,要打开设备或者端口的实例,需要使用设备或端口的名称

PeripheralManagerService manager = new PeripheralManagerService();
List deviceList = manager.getI2sDeviceList();
if (deviceList.isEmpty()) {
    Log.i(TAG, "No I2S bus available on this device.");
} else {
    Log.i(TAG, "List of available devices: " + deviceList);
}

2. I2S设备的代开和关闭
当我们列举出每个I2S设备的名称后,就可以打开指定的设备了。但因为I2S是专用于音频传输的总线,所以打开I2S总线上的设备时,需要同时传入一个音频配置,使Android Things能够正确的解析和打包在I2S总线上传输的数据流。音频配置的具体说明请看下一节的1.音频配置

public class HomeActivity extends Activity {
    private static final String I2S_DEVICE_NAME = ...;
    private static final AudioFormat AUDIO_FORMAT_STEREO =
            new AudioFormat.Builder()
            .setChannelMask(AudioFormat.CHANNEL_IN_STEREO)
            .setEncoding(AudioFormat.ENCODING_PCM_16BIT)
            .setSampleRate(44100)
            .build();

    private I2sDevice mDevice;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // 用指定的音频配置去打开I2S设备
        try {
            PeripheralManagerService manager = new PeripheralManagerService();
            mDevice = manager.openI2sDevice(I2S_DEVICE_NAME, AUDIO_FORMAT_STEREO);
        } catch (IOException e) {
            Log.w(TAG, "Unable to access I2S device", e);
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();

        // 关闭I2S设备
        if (mDevice != null) {
            try {
                mDevice.close();
                mDevice = null;
            } catch (IOException e) {
                Log.w(TAG, "Unable to close I2S device", e);
            }
        }
    }
}

你可以在任何时候去打开和关闭I2S设备,不必像上面示例代码中那样在onCreateonDestory中去操作。

3. I2S设备的数据读写

public int obtainAudio(String sourceI2sName, String sinkI2sName, AudioFormat format) throws IOException {
    // 打开音频输入源和输出源
    PeripheralManagerService manager = new PeripheralManagerService();
    I2sDevice source = manager.openI2sDevice(sourceI2sName, format);
    I2sDevice sink = manager.openI2sDevice(sinkI2sName, format);

    // 计算最小音频缓存
    int bufferSize = AudioTrack.getMinBufferSize(
            format.getSampleRate(),
            format.getChannelMask(),
            format.getEncoding());

    // 把音频输入源的数据传送到输出源
    ByteBuffer buffer = ByteBuffer.allocate(bufferSize);
    int read = source.read(buffer, bufferSize);
    sink.write(buffer, read);

    // 关闭音频输入源
    try {
        source.close();
    } catch (IOException e) {
        Log.w(TAG, "Unable to close source I2S device", e);
    }

    // 关闭音频输出源 
    try {
        sink.close();
    } catch (IOException e) {
        Log.w(TAG, "Unable to close sink I2S device", e);
    }

    return read;
}

上面的示例代码中,把I2S输入设备的数据流取出来转发给了I2S输出设备。
注:

  • I2S输入设备和I2S输出设备都应该使用相同音频配置,除非你会在转发的过程中转换PCM数据流的采样率、比特率等参数
  • 实际项目中无需每次读写都去开关I2S设备,为了效率,仅仅在使用前后打开关闭I2S设备即可
  • 中间的数据读写也是可以在循环中不断操作的
  • 所有操作均应该在异步线程中执行,数据转发和网络操作一样,都是耗时操作,不应该在主线程中执行,以免阻塞UI和事件

使用I2S设备

1. 音频配置
音频配置在Android Things中对应的类为:

AudioFormat
AudioFormat.Builder

对于I2S音频的配置,主要使用要指定下面3个参数:

  • channel - 声道,支持单声道、立体声、立体声+中置声道、四声道、四声道+中置声道、5.1声道、5.1声道+后中置声道、7.1声道
  • encoding - 编码格式,支持PCM(8bit/16bit/float)、DTS、AC3
  • sampleRate - 采样率,支持8kHz~192kHz

你可以用AudioFormat.Builder类创建一个音频配置实例,并指定上面所述的参数,上面所述的参数都能在AudioFormat中找到对应的常量

2. 向从设备播放音频
这里一般有三种播放场景:

  1. 播放效果音、提示音等简短的固定音频
  2. 播放音乐、视频等流式音频
  3. 播放全系统的音频流

原则上,只要往I2sDevice对象上write(byte[] data)就可以让I2S设备播放出声音来,但整个系统中的声音如果都想传给I2S设备来播放,就得注册一个UserDrvier给UserDriverManager来供系统调用

2.1 播放固定音频

public void soundOutputAudio(String i2sName, AudioFormat format, byte[] pcm) throws IOException {
    PeripheralManagerService manager = new PeripheralManagerService();
    I2sDevice sink = manager.openI2sDevice(i2sName, format);
    sink.write(pcm, pcm.length);
    sink.close();
}

重申:并不需要每次向I2S设备播放声音的时候都open/close一次I2S设备,可以在使用前open一次I2S设备,直到不再使用I2S设备时再close

2.2 播放媒体音频
这部分可以配合MediaCodec把媒体文件的音频流解码出PCM数据,然后使用2.1节所示的方法循环调用sink.write(pcm, pcm.length);写入到I2S设备去播放。因为代码烦杂,而且MediaCodec也不属于I2S范畴,本萌也比较懒,所以这部分代码就不给出了,有兴趣的同学自己去小黑屋里研究吧~

啊~哈哈哈哈哈……

2.3 播放系统音频流
想要整个系统中的声音都传给I2S设备来播放,就得注册一个UserDrvier给UserDriverManager来供系统调用。Android Things提供了AudioOutputDriver类来实现这个功能。

首先你需要派生一下AudioOutputDriver来实现I2S设备的驱动

public class I2sPlaybackDriver extends AudioOutputDriver {
    private I2sDevice mOutputDevice;

    public PlaybackDriver(String i2sName, AudioFormat format) {
        PeripheralManagerService manager = new PeripheralManagerService();
        mOutputDevice = manager.openI2sDevice(i2sName, format);
    }

    @Override
    public void onStandbyChanged(boolean inStandby) {
        if (inStandby) {
            // 现在需要让I2S设备处于待机模式
            // 如果I2S设备支持,你可以在这里设置I2S设备的电源模式是待机模式,或者设置I2S设备为静音模式
        } else {
            // 让I2S设备退出待机模式,随时准备播放音频
        }
    }

    @Override
    public int write(ByteBuffer buffer, int count) {
        try {
            return mOutputDevice.write(buffer, count);
        } catch (IOException e) {
            Log.w(TAG, "Unable to write audio buffer", e);
            return -1;
        }
    }

    public void terminate() {
        if (mOutputDevice != null) {
            try {
                mOutputDevice.close();
            } catch (IOException e) {
                Log.w(TAG, "Unable to close sink I2S device", e);
            }
        }
    }
}

然后在Android Things的应用中使用这个驱动

public class AudioDriverService extends Service {
    private static final String I2S_DEVICE_NAME = ...;

    // 设定默认的音频配置
    private static final AudioFormat AUDIO_FORMAT_STEREO =
            new AudioFormat.Builder()
            .setChannelMask(AudioFormat.CHANNEL_IN_STEREO) // 立体声
            .setEncoding(AudioFormat.ENCODING_PCM_16BIT) // 16Bit PCM输出
            .setSampleRate(44100) // 44.1kHz采样率
            .build();

    private I2sPlaybackDriver mPlaybackDriver;

    @Override
    public void onCreate() {
        super.onCreate();

        mPlaybackDriver = new I2sPlaybackDriver(I2S_DEVICE_NAME);
        UserDriverManager manager = UserDriverManager.getManager();

        // 计算默认音频配置所需的最小缓存
        int bufferSize = AudioTrack.getMinBufferSize(
            AUDIO_FORMAT_STEREO.getSampleRate(),
            AUDIO_FORMAT_STEREO.getChannelMask(),
            AUDIO_FORMAT_STEREO.getEncoding());

        // 注册I2S音频驱动到驱动管理器
        manager.registerAudioOutputDriver(
               mPlaybackDriver, // I2S音频驱动
                AUDIO_FORMAT_STEREO, // 音频配置
                AudioDeviceInfo.TYPE_BUILTIN_SPEAKER, // 指定I2S音频驱动为内建扬声器设备
                bufferSize); // 指定音频驱动所需的缓冲区大小
    }

    @Override
    public void onDestroy() {
        super.onDestroy();

        // 从驱动管理器中注销I2S音频驱动,并释放该驱动所占用的资源和I2S总线
        UserDriverManager manager = UserDriverManager.getManager();
        manager.unregisterAudioOutputDriver(mPlaybackDriver);
        mPlaybackDriver.termiate();
        mPlaybackDriver = null;
    }
}

注意:UserDriverManager.registerAudioOutputDriver()方法是需要在Manifest中声明android.permission.MODIFY_AUDIO_SETTINGS权限的,而Android Things中没有继承用户授权操作,如果应用有使用到非Normal Permissions,则需要重启树莓派后权限才能生效

3. 向从设备播放本地实时采集的音频
采集本地的模拟音频到I2S设备上去处理,关键操作就是把模拟音频转换为I2S能够识别的数字音频格式,比如PCM、DTS、AC3等。那么秘诀就在于使用下面这个类:

AudioRecord
这个类可以采集的实时音频很多,麦克风、通话对方的语音、通话己方的语音等等

我们以采集系统麦克风为例,具体的实现思路如下:

public void soundMicAudio(String i2sName, AudioFormat format) throws IOException {
    // 打开I2S播放设备
    PeripheralManagerService manager = new PeripheralManagerService();
    I2sDevice sink = manager.openI2sDevice(i2sName, format);

    // 计算本地音频采集所需的最小缓冲大小
    int bufferSize = AudioRecord.getMinBufferSize(
            format.getSampleRate(),
            format.getChannelMask(),
            format.getEncoding());
    AudioRecord recorder = new AudioRecord(
            MediaRecorder.AudioSource.MIC,
            format.getSampleRate(),
            format.getChannelMask(),
            format.getEncoding(),
            bufferSize);

    // 准备音频流转发缓冲
    ByteBuffer buffer = ByteBuffer.allocate(bufferSize);

    // 启动音频采集
    recorder.startRecording();

    // 开始音频流转发
    int readCount = 0;
    while (mIsRecording) {
        if ((readCount = recorder.read(buffer, bufferSize)) > 0) {
            sink.write(buffer, readCount);
        }
    }

    // 停止音频采集
    recorder.stop();
    recorder.release();

    // 关闭I2S播放设备
    sink.close();
}

4. 录制从设备传入的音频
首先~"从设备"是一个词,它是接在"主设备"上的设备,它也是I2S设备←_←
I2S总线上传输的数据流永远都是PCM、DTS、AC3等编码的数字音频数据,原则上直接保存这些数据到文件流可以。

  • PCM,对应的文件就是WAV
  • DTS,对应的文件就是DTS,但有的文件也可以是WAV,它们拥有兼容的封装格式
  • AC3,对应的文件很多,有AC3、AAC、MP4、M4A、MPEG等多种文件封装

从从设备传入的音频数据已经是编码好的,但如果是DTS和AC3格式的音频,直接添加文件头后保存就可以,如果是PCM格式的音频,直接保存会很大,可以再通过MediaCodec去编码成更小巧的音频格式来保存,如MP3
基本实现思路如下:

public void recordInputAudio(String i2sName, AudioFormat format, OutputStream fout) throws IOException {
    // 打开I2S输入设备
    PeripheralManagerService manager = new PeripheralManagerService();
    I2sDevice source = manager.openI2sDevice(i2sName, format);

    // 配置编解码器为编码模式
    MediaCodec codec = MediaCodec.createEncoderByType("audio/mp3");
    MediaFormat codecFormat = MediaFormat.createAudioFormat("audio/mp3", format.getSampleRate(), format.getChannelCount());
    codecFormat.setInteger(..., ...); // 设置各种编码参数
    codec.configure(codecFormat, ..., MediaCodec.CONFIGURE_FLAG_ENCODE);

    // 开始编码
    codec.start();

    // 开始音频流编码保存
    int readCount = 0;
    while (mIsRecording) {
        if ((readCount = recorder.read(buffer, bufferSize)) > 0) {
            // 获取编码缓冲
            int inputBufferIndex = codec.dequeueInputBuffer(-1);
            // 填充数据
            codec.getInputBuffers()[inputBufferIndex].put(...);
            // 编码
            codec.queueInputBuffer(inputBufferIndex, 0, readCount, 0, 0);

            // 获取编码流
            int outputBufferIndex = -1;
            MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
            while((outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, 0)) > -1) {
                 ByteBuffer outputBuffer = codec.getOutputBuffers()[outputBufferIndex];
                 // 把编码流写入文件流
                 fout.write(outputBuffer, ...);
           }
        }
    }

    // 停止编码
    codec.stop();
    codec.release();

    // 关闭I2S播放设备
    source.close();
}

代码中对Buffer的优化大家自己去做,我这里就把思路点到为止

5. 播放从设备传入的音频

public void soundInputAudio(String i2sName, AudioFormat format) throws IOException {
    // 打开I2S输入设备
    PeripheralManagerService manager = new PeripheralManagerService();
    I2sDevice source = manager.openI2sDevice(i2sName, format);

    // 设备本地音频播放
    int bufferSize = AudioTrack.getMinBufferSize(
            format.getSampleRate(),
            format.getChannelMask(),
            format.getEncoding());
    AudioTrack sink = new AudioTrack.Builder()
            .setAudioFormat(format)
            .setTransferMode(AudioTrack.MODE_STREAM)
            .setBufferSizeInBytes(bufferSize)
            .build();

    // 准备音频转发缓冲
    ByteBuffer buffer = ByteBuffer.allocate(bufferSize);

    // 开始音频流转发
    int readCount = 0;
    while ((readCount = source.read(buffer, bufferSize)) > 0) {
        sink.write(buffer, readCount);
    }

    // 关闭I2S输入输出音频设备
    sink.release();
    source.close();
}

:本文中的代码都还是纯理论的,本萌正在积极的攒钱购买I2S解码器来验证这些代码,请大家不要迷信本文~

[Android Things] I2S_第3张图片
做个智能KTV欢脱吧

你可能感兴趣的:([Android Things] I2S)