question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Keep reference to audio processor (ChannelMappingAudioProcessor) without creating new SimpleExoPlayer instance everytime

See original GitHub issue

### [REQUIRED] Issues. Need help change channel audio mapping processor while media on playing (on the fly).

### [REQUIRED] Searched documentation. I’ve search all documents related to ChannelMappingAudioProcessor : 2659 6288 6415 The only hint close to my issue is 6415 but i still don’t know where to start implement it from @andrewlewis comment.

### [REQUIRED] Question Keep reference to audio processor (ChannelMappingAudioProcessor) without creating new SimpleExoPlayer instance everytime

How can i keep a reference to audio processor when created and interact with the app? also synchronize access to its dynamic configuration. So i can change the audio channel while media still playing (on the fly).

@andrewlewis comment on #6415

If the output format from the mapping is staying the same (including the same number of channels) you can just keep a reference to your audio processor when you create it, and interact with it from your app. You will need to make sure you synchronize access to its dynamic configuration because ExoPlayer will use the processor on its internal playback thread. Note: the audio track buffer is downstream from the audio processor so modifications won’t be reflected in output audio instantly.

My testing code :

Structure:

  • AudioProcessor interface

  • BaseAudioProcessor class

  • ChannelMappingAudioProcessor class :

final class ChannelMappingAudioProcessor extends BaseAudioProcessor {

    @Nullable private int[] pendingOutputChannels;
    @Nullable private int[] outputChannels;

    /**
     * Resets the channel mapping. After calling this method, call {@link #configure(AudioFormat)} to
     * start using the new channel map.
     *
     * @param outputChannels The mapping from input to output channel indices, or {@code null} to
     *     leave the input unchanged.
     * @see AudioSink#configure(int, int, int, int, int[], int, int)
     */
    public void setChannelMap(@Nullable int[] outputChannels) {
        pendingOutputChannels = outputChannels;
    }

    @Override
    public AudioFormat onConfigure(AudioFormat inputAudioFormat)
            throws UnhandledAudioFormatException {
        @Nullable int[] outputChannels = pendingOutputChannels;
        if (outputChannels == null) {
            return AudioFormat.NOT_SET;
        }

        if (inputAudioFormat.encoding != C.ENCODING_PCM_16BIT) {
            throw new UnhandledAudioFormatException(inputAudioFormat);
        }

        boolean active = inputAudioFormat.channelCount != outputChannels.length;
        for (int i = 0; i < outputChannels.length; i++) {
            int channelIndex = outputChannels[i];
            if (channelIndex >= inputAudioFormat.channelCount) {
                throw new UnhandledAudioFormatException(inputAudioFormat);
            }
            active |= (channelIndex != i);
        }
        return active
                ? new AudioFormat(inputAudioFormat.sampleRate, outputChannels.length, C.ENCODING_PCM_16BIT)
                : AudioFormat.NOT_SET;
    }

    @Override
    public void queueInput(ByteBuffer inputBuffer) {
        int[] outputChannels = Assertions.checkNotNull(this.outputChannels);
        int position = inputBuffer.position();
        int limit = inputBuffer.limit();
        int frameCount = (limit - position) / inputAudioFormat.bytesPerFrame;
        int outputSize = frameCount * outputAudioFormat.bytesPerFrame;
        ByteBuffer buffer = replaceOutputBuffer(outputSize);
        while (position < limit) {
            for (int channelIndex : outputChannels) {
                buffer.putShort(inputBuffer.getShort(position + 2 * channelIndex));
            }
            position += inputAudioFormat.bytesPerFrame;
        }
        inputBuffer.position(limit);
        buffer.flip();
    }

    @Override
    protected void onFlush() {
        outputChannels = pendingOutputChannels;
    }

    @Override
    protected void onReset() {
        outputChannels = null;
        pendingOutputChannels = null;
    }
}

All of them i copied from exoplayer github project structure in core library (audio) Testing use 1 button to switch the L + R channel to L+L. Note: i don’t need ability to change the volume on separate channel (basic only)

  • MainActivity class

Method to initialize instance:

private void PrepareExoPlayerUri() {
        try {
            trackSelectionFactory = new AdaptiveTrackSelection.Factory();

            inputAudioFormat = new AudioFormat(
                    44100, 2, C.ENCODING_PCM_16BIT
            );
            channelMappingAudioProcessor = new ChannelMappingAudioProcessor();
            channelMappingAudioProcessor.setChannelMap(new int[]{0, 1}); // left + Right channel
            channelMappingAudioProcessor.onConfigure(inputAudioFormat);

            DefaultRenderersFactory renderersFactory = new DefaultRenderersFactory(this) {
                @Override
                protected AudioProcessor[] buildAudioProcessors() {
                    audioProcessor = new AudioProcessor[]{channelMappingAudioProcessor};
                    return audioProcessor;
                }
            };

            trackSelectorParameters = new DefaultTrackSelector.ParametersBuilder(this).build();
            trackSelector = new DefaultTrackSelector(this, trackSelectionFactory);
            trackSelector.setParameters(trackSelectorParameters);

            // using uri data source
            dataSourceFactory = new DefaultDataSourceFactory(this,
                    Util.getUserAgent(this, "exoplayer_video"));

            // using uri data source
            mediaSource = new ProgressiveMediaSource.Factory(dataSourceFactory)
                    .createMediaSource(videouri);

            player = new SimpleExoPlayer.Builder(this, renderersFactory)
                    .setTrackSelector(trackSelector)
                    .build();
            player.prepare(mediaSource);
            playerView.setPlayer(player);
            player.setPlayWhenReady(true);
            player.addListener(playerListener);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

Button click trigger to change audio channel while media still playing :

btnSwitchChannel.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {

            try {
                inputAudioFormat = new AudioFormat(
                        44100, 2, C.ENCODING_PCM_16BIT
                );
                channelMappingAudioProcessor = new ChannelMappingAudioProcessor();
                channelMappingAudioProcessor.setChannelMap(new int[]{0, 0}); // left + left channel
                channelMappingAudioProcessor.onConfigure(inputAudioFormat);

                DefaultRenderersFactory renderersFactory = new DefaultRenderersFactory(getApplicationContext()) {
                    @Override
                    protected AudioProcessor[] buildAudioProcessors() {
                        audioProcessor = new AudioProcessor[]{channelMappingAudioProcessor};
                        return audioProcessor;
                    }
                };
                
                player = new SimpleExoPlayer.Builder(getApplicationContext(), renderersFactory)
                        .setTrackSelector(trackSelector)
                        .build();
                player.setPlayWhenReady(true);

            } catch (Exception e) {
                e.printStackTrace();
            }

        }
    });

From code above i’m still don’t understand how to change the outputChannel with new parameter on ChannelMappingAudioProcessor setChannelMap method. And how we can interact with audio processor reference on player instance without release the player object and create new player instance

Note: output format same (including number of channels)

Please correct and help me. Thanks.

Issue Analytics

  • State:closed
  • Created 3 years ago
  • Comments:8

github_iconTop GitHub Comments

1reaction
Skys777commented, Apr 18, 2021

@lcf87 Thanks for your help.

after came back to this issue again, i finally figure it out.

hopefully i can help with someone encounter with the same issue as me.

for exo 2.11.8 we can set in buildAudioProcessor by changing with new channel map and call flush method quote from exoplayer dev : Clears any buffered data and pending output. If the audio processor is active, also prepares the audio processor to receive a new stream of input in the last configured (pending) format.

so the flush method that i’m missed before.

for 2.12.x we set the ChannelMappingAudioProcessor in AudioSink. same approach, call setChannelMap and the flush the audio buffer data and pendingOutputChannel.

example code:

channelMappingAudioProcessor = new ChannelMappingAudioProcessor(); channelMappingAudioProcessor.setChannelMap(new int[]{0, 0}); -> int array depends on your media channel layout

renderersFactory = new DefaultRenderersFactory(this) { @Nullable @Override protected AudioSink buildAudioSink(Context context, boolean enableFloatOutput, boolean enableAudioTrackPlaybackParams, boolean enableOffload) {

                audioProcessor = new AudioProcessor[] {channelMappingAudioProcessor};

                return new DefaultAudioSink(
                        AudioCapabilities.DEFAULT_AUDIO_CAPABILITIES,
                        new DefaultAudioSink.DefaultAudioProcessorChain(audioProcessor).getAudioProcessors()
                );
            }
        }

i get this solution from another reference #8216 . thank to @timusus for pointing this out.

0reactions
lcf87commented, Jan 15, 2021

Sorry for the inactivity.

It seems to me that creating a player every time you hit a button isn’t the best choice, I was suggesting something that moves the audio processor, render factory and player creation out of the onClickLIstener, something like

@Override 
public void onStart() {
            try {
                inputAudioFormat = new AudioFormat(
                        44100, 2, C.ENCODING_PCM_16BIT
                );
                // Can also be instance var.
                channelMappingAudioProcessor = new ChannelMappingAudioProcessor();
                channelMappingAudioProcessor.setChannelMap(new int[]{0, 0}); // left + left channel
                channelMappingAudioProcessor.onConfigure(inputAudioFormat);

                DefaultRenderersFactory renderersFactory = new DefaultRenderersFactory(getApplicationContext()) {
                    @Override
                    protected AudioProcessor[] buildAudioProcessors() {
                        audioProcessor = new AudioProcessor[]{channelMappingAudioProcessor};
                        return audioProcessor;
                    }
                };
                
                player = new SimpleExoPlayer.Builder(getApplicationContext(), renderersFactory)
                        .setTrackSelector(trackSelector)
                        .build();
                player.setPlayWhenReady(true);

            } catch (Exception e) {
                e.printStackTrace();
            }
     btnSwitchChannel.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
                channelMappingAudioProcessor.setChannelMap(new int[]{0, 0}); // left + left channel
        }
    });
}
Read more comments on GitHub >

github_iconTop Results From Across the Web

How to modify audio processors on the fly - Stack Overflow
I want to be able to access the ChannelMappingAudioProcessor and reconfigure it on the fly, so that I can change the channel mapping...
Read more >
mozilla-central: changeset 360314 ...
Bug 1341990 - Part 1: Import ExoPlayer sources (r2.4.0) without {ui/ ... /google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java ...
Read more >
SimpleExoPlayer (ExoPlayer library)
Modifier and Type Method Description void decreaseDeviceVolume() Deprecated. Decreases the volume of the devi... Clock getClock() Deprecated. Returns the Clock used for playb... CueGroup getCurrentCues() Deprecated....
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found