如何在安卓设备上播放本地相机声音?

32

我想在相机预览捕获时播放本地相机快门声音剪辑。 我指的是调用takePicture()时播放的声音剪辑。
我怎么做呢? 有人能为我解释一下步骤吗?

6个回答

67
您可以使用 MediaActionSound 类(API 16 及以上版本可用)。例如:
MediaActionSound sound = new MediaActionSound();
sound.play(MediaActionSound.SHUTTER_CLICK);

然而,由于某种令人费解的原因,制作此类的Google开发人员决定以100%音量播放声音。不幸的是,该类中的许多内容都是私有的,并且无法轻松地通过扩展来增强它。这就是为什么我创建了一个修改版本(转换为Kotlin)的此类,该版本尊重给定流音量(例如媒体、通知等)或允许设置任意音量。请注意,不能保证这不会在将来发生故障,因为系统声音的文件路径是硬编码的。另一方面,最糟糕的影响可能是无法播放声音。我还将为此创建单元测试,如果出现故障,我将在此更新代码。
只需调用playWithStreamVolume以使用流音量,或者使用可选的任意音量调用play。不要忘记更新包。
/*
 * Copyright (C) 2012 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.your.package

import android.content.Context
import android.content.Context.AUDIO_SERVICE
import android.media.AudioAttributes
import android.media.AudioManager
import android.media.SoundPool
import android.util.Log
import java.lang.IllegalStateException

/**
 * Modified by Miloš Černilovský on March 16 2021: converted original class to Kotlin,
 * fixed a minor bug and added support for respecting stream volume or setting an arbitrary
 * volume. New methods added: [playWithStreamVolume], volume parameters added to [play].
 *
 * A class for producing sounds that match those produced by various actions
 * taken by the media and camera APIs.
 *
 * This class is recommended for use with the [android.hardware.camera2] API, since the
 * camera2 API does not play any sounds on its own for any capture or video recording actions.
 *
 * With the older [android.hardware.Camera] API, use this class to play an appropriate
 * camera operation sound when implementing a custom still or video recording mechanism (through the
 * Camera preview callbacks with
 * [Camera.setPreviewCallback][android.hardware.Camera.setPreviewCallback], or through GPU
 * processing with [Camera.setPreviewTexture][android.hardware.Camera.setPreviewTexture], for
 * example), or when implementing some other camera-like function in your application.
 *
 * There is no need to play sounds when using
 * [Camera.takePicture][android.hardware.Camera.takePicture] or
 * [android.media.MediaRecorder] for still images or video, respectively,
 * as the Android framework will play the appropriate sounds when needed for
 * these calls.
 */
@Suppress("MemberVisibilityCanBePrivate", "unused")
class MediaActionSound {
    private val sounds: Array<SoundState> = SOUND_FILES.indices.map {
        SoundState(it)
    }.toTypedArray()

    private val loadCompleteListener = SoundPool.OnLoadCompleteListener { soundPool, sampleId, status ->
        for (sound in sounds) {
            if (sound.id != sampleId) {
                continue
            }
            var playSoundId = 0
            synchronized(sound) {
                if (status != 0) {
                    sound.state = STATE_NOT_LOADED
                    sound.id = 0
                    Log.e(TAG, "OnLoadCompleteListener() error: " + status +
                            " loading sound: " + sound.name)
                    return@OnLoadCompleteListener
                }
                when (sound.state) {
                    STATE_LOADING -> sound.state = STATE_LOADED
                    STATE_LOADING_PLAY_REQUESTED -> {
                        playSoundId = sound.id
                        sound.state = STATE_LOADED
                    }
                    else -> Log.e(TAG, "OnLoadCompleteListener() called in wrong state: "
                            + sound.state + " for sound: " + sound.name)
                }
            }
            if (playSoundId != 0) {
                soundPool.play(playSoundId, sound.volumeLeft, sound.volumeRight, 0, 0, 1.0f)
            }
            break
        }
    }

    private var _soundPool: SoundPool? = SoundPool.Builder()
            .setMaxStreams(NUM_MEDIA_SOUND_STREAMS)
            .setAudioAttributes(AudioAttributes.Builder()
                    .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION)
                    .setFlags(AudioAttributes.FLAG_AUDIBILITY_ENFORCED)
                    .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
                    .build())
            .build().also {
                it.setOnLoadCompleteListener(loadCompleteListener)
            }

    private val soundPool: SoundPool
        get() {
            return _soundPool ?: throw IllegalStateException("SoundPool has been released. This class mustn't be used after release() is called.")
        }

    private inner class SoundState(val name: Int) {
        var id = 0

        // 0 is an invalid sample ID.
        var state: Int = STATE_NOT_LOADED
        var volumeLeft: Float = 1f
        var volumeRight: Float = 1f
    }

    /**
     * Construct a new MediaActionSound instance. Only a single instance is
     * needed for playing any platform media action sound; you do not need a
     * separate instance for each sound type.
     */
    @Suppress("ConvertSecondaryConstructorToPrimary", "RemoveEmptySecondaryConstructorBody")
    constructor() {
    }

    private fun loadSound(sound: SoundState): Int {
        val soundFileName = SOUND_FILES[sound.name]
        for (soundDir in SOUND_DIRS) {
            val id = soundPool.load(soundDir + soundFileName, 1)
            if (id > 0) {
                sound.state = STATE_LOADING
                sound.id = id
                return id
            }
        }
        return 0
    }

    /**
     * Preload a predefined platform sound to minimize latency when the sound is
     * played later by [playWithStreamVolume].
     * @param soundName The type of sound to preload, selected from
     * SHUTTER_CLICK, FOCUS_COMPLETE, START_VIDEO_RECORDING, or
     * STOP_VIDEO_RECORDING.
     * @return True if the sound was successfully loaded.
     * @see playWithStreamVolume
     * @see SHUTTER_CLICK
     * @see FOCUS_COMPLETE
     * @see START_VIDEO_RECORDING
     * @see STOP_VIDEO_RECORDING
     */
    fun load(soundName: Int): Boolean {
        if (soundName < 0 || soundName >= sounds.size) {
            throw RuntimeException("Unknown sound requested: $soundName")
        }
        val sound = sounds[soundName]
        return synchronized(sound) {
            when (sound.state) {
                STATE_NOT_LOADED -> {
                    if (loadSound(sound) <= 0) {
                        Log.e(TAG, "load() error loading sound: $soundName")
                        false
                    } else {
                        true
                    }
                }
                else -> {
                    Log.e(TAG, "load() called in wrong state: $sound for sound: $soundName")
                    false
                }
            }
        }
    }

    /**
     * Attempts to retrieve [AudioManager] from the given [context] and plays the given sound with the given [streamType] volume.
     * If retrieving volume is not successful, [defaultVolume] is used. Finally calls [play].
     * @param streamType One of [AudioManager] constants beginning with "STREAM_" prefix, e. g. [AudioManager.STREAM_MUSIC]
     */
    fun playWithStreamVolume(soundName: Int, context: Context, streamType: Int = AudioManager.STREAM_MUSIC, defaultVolume: Float = 1f) {
        playWithStreamVolume(soundName, context.getSystemService(AUDIO_SERVICE) as AudioManager?, streamType, defaultVolume)
    }

    /**
     * Plays the given sound with the given [streamType] volume. If retrieving volume is not successful,
     * [defaultVolume] is used. Finally calls [play].
     * @param streamType One of [AudioManager] constants beginning with "STREAM_" prefix, e. g. [AudioManager.STREAM_MUSIC]
     */
    fun playWithStreamVolume(soundName: Int, audioManager: AudioManager?, streamType: Int = AudioManager.STREAM_MUSIC, defaultVolume: Float = 1f) {
        val volume = audioManager?.let { it.getStreamVolume(streamType) / it.getStreamMaxVolume(streamType).toFloat() } ?: defaultVolume
        play(soundName, volume, volume)
    }

    /**
     * Play one of the predefined platform sounds for media actions.
     *
     * Use this method to play a platform-specific sound for various media
     * actions. The sound playback is done asynchronously, with the same
     * behavior and content as the sounds played by
     * [Camera.takePicture][android.hardware.Camera.takePicture],
     * [MediaRecorder.start][android.media.MediaRecorder.start], and
     * [MediaRecorder.stop][android.media.MediaRecorder.stop].
     *
     * With the [camera2][android.hardware.camera2] API, this method can be used to play
     * standard camera operation sounds with the appropriate system behavior for such sounds.
     *
     * With the older [android.hardware.Camera] API, using this method makes it easy to
     * match the default device sounds when recording or capturing data through the preview
     * callbacks, or when implementing custom camera-like features in your application.
     *
     * If the sound has not been loaded by [load] before calling play,
     * play will load the sound at the cost of some additional latency before
     * sound playback begins.
     *
     * @param soundName The type of sound to play, selected from
     * [SHUTTER_CLICK], [FOCUS_COMPLETE], [START_VIDEO_RECORDING], or
     * [STOP_VIDEO_RECORDING].
     * @param leftVolume left volume value (range = 0.0 to 1.0)
     * @param rightVolume right volume value (range = 0.0 to 1.0)
     * @see android.hardware.Camera.takePicture
     * @see android.media.MediaRecorder
     * @see SHUTTER_CLICK
     * @see FOCUS_COMPLETE
     * @see START_VIDEO_RECORDING
     * @see STOP_VIDEO_RECORDING
     */
    @JvmOverloads // for backward Java compatibility
    fun play(soundName: Int, leftVolume: Float = 1f, rightVolume: Float = leftVolume) {
        if (soundName < 0 || soundName >= SOUND_FILES.size) {
            throw RuntimeException("Unknown sound requested: $soundName")
        }
        val sound = sounds[soundName]
        synchronized(sound) {
            when (sound.state) {
                STATE_NOT_LOADED -> {
                    if (loadSound(sound) <= 0) {
                        Log.e(TAG, "play() error loading sound: $soundName")
                    } else {
                        setRequestPlayStatus(sound, leftVolume, rightVolume)
                    }
                }
                STATE_LOADING -> setRequestPlayStatus(sound, leftVolume, rightVolume)
                STATE_LOADED -> soundPool.play(sound.id, leftVolume, rightVolume, 0, 0, 1.0f)
                else -> Log.e(TAG, "play() called in wrong state: " + sound.state + " for sound: " + soundName)
            }
        }
    }

    private fun setRequestPlayStatus(sound: SoundState, leftVolume: Float, rightVolume: Float) {
        with(sound) {
            state = STATE_LOADING_PLAY_REQUESTED
            volumeLeft = leftVolume
            volumeRight = rightVolume
        }
    }

    /**
     * Free up all audio resources used by this MediaActionSound instance. Do
     * not call any other methods on a MediaActionSound instance after calling
     * release().
     */
    fun release() {
        _soundPool?.let {
            for (sound in sounds) {
                synchronized(sound) {
                    sound.state = STATE_NOT_LOADED
                    sound.id = 0
                }
            }
            it.release()
            _soundPool = null
        }
    }

    companion object {
        private const val NUM_MEDIA_SOUND_STREAMS = 1
        private val SOUND_DIRS = arrayOf(
                "/product/media/audio/ui/",
                "/system/media/audio/ui/")
        private val SOUND_FILES = arrayOf(
                "camera_click.ogg",
                "camera_focus.ogg",
                "VideoRecord.ogg",
                "VideoStop.ogg"
        )
        private const val TAG = "MediaActionSound"

        /**
         * The sound used by
         * [Camera.takePicture][android.hardware.Camera.takePicture] to
         * indicate still image capture.
         * @see playWithStreamVolume
         */
        const val SHUTTER_CLICK = 0

        /**
         * A sound to indicate that focusing has completed. Because deciding
         * when this occurs is application-dependent, this sound is not used by
         * any methods in the media or camera APIs.
         * @see playWithStreamVolume
         */
        const val FOCUS_COMPLETE = 1

        /**
         * The sound used by
         * [MediaRecorder.start()][android.media.MediaRecorder.start] to
         * indicate the start of video recording.
         * @see playWithStreamVolume
         */
        const val START_VIDEO_RECORDING = 2

        /**
         * The sound used by
         * [MediaRecorder.stop()][android.media.MediaRecorder.stop] to
         * indicate the end of video recording.
         * @see playWithStreamVolume
         */
        const val STOP_VIDEO_RECORDING = 3

        /**
         * States for SoundState.
         * STATE_NOT_LOADED             : sample not loaded
         * STATE_LOADING                : sample being loaded: waiting for load completion callback
         * STATE_LOADING_PLAY_REQUESTED : sample being loaded and playback request received
         * STATE_LOADED                 : sample loaded, ready for playback
         */
        private const val STATE_NOT_LOADED = 0
        private const val STATE_LOADING = 1
        private const val STATE_LOADING_PLAY_REQUESTED = 2
        private const val STATE_LOADED = 3
    }
}

我也向Google提供了这个类,请为此问题点赞。


6
这个方法没有音量控制?无论设备上的音量设置为多少,都会以最大音量播放快门声。请问需要翻译的还有其他内容吗? - Hashman
2
它应该自动尊重用户当前的通知音量。 - Greyson Parrelli
播放音频后,我们需要释放Soundpool吗? - Vo Thanh Tung
1
@VoThanhTung 如果您不打算再次使用MediaActionSound实例,我认为应该将其释放。 - Miloš Černilovský
@Hashman,我已经在答案中添加了MediaActionSound的修改版本,它允许设置音量或遵守给定流的音量。 - Miloš Černilovský
显示剩余2条评论

19

如果系统文件存在,您可以像这样使用它:

public void shootSound()
{
    AudioManager meng = (AudioManager) getContext().getSystemService(Context.AUDIO_SERVICE);
    int volume = meng.getStreamVolume( AudioManager.STREAM_NOTIFICATION);

    if (volume != 0)
    {
        if (_shootMP == null)
            _shootMP = MediaPlayer.create(getContext(), Uri.parse("file:///system/media/audio/ui/camera_click.ogg"));
        if (_shootMP != null)
            _shootMP.start();
    }
}

2
@Derzu:在我的情况下,_sootMP 是相机 Activity 的一个类成员。没有必要为每个捕获重新创建它。 - Alex Cohn
1
在您的情况下很棒,但读者仍需要知道它是什么。 - Rob Pridham

9

您可能想要使用SoundPool

SoundPool soundPool = new SoundPool(1, AudioManager.STREAM_NOTIFICATION, 0);
int shutterSound = soundPool.load(this, R.raw.camera_click, 0);

然后播放声音

soundPool.play(shutterSound, 1f, 1f, 0, 0, 1);

请查看http://developer.android.com/reference/android/media/SoundPool.html以了解参数。
您需要在项目的res/raw中添加名为camera_click.ogg的媒体文件。如果您的项目是根据Apache许可证授权的,则可以使用Android默认声音,该声音可以从Android开源项目的以下位置获取(frameworks/base/data/sounds/effects/camera_click.ogg)。如果您的项目未根据Apache许可证授权,则我不知道您是否可以使用它。我不是律师。

从文档中得知:"priority field" 表示声音的优先级。目前没有任何影响。为了未来兼容性,请使用值为1。 - greg7gkb
Apache许可证非常宽松,您可以在任何项目中使用其资源。另一方面,可以查找设备上已经可用的声音。 - Alex Cohn

3

0

这段代码将帮助你在铃声模式正常而非静音或振动时播放声音。

private var audioManager: AudioManager? = null
private var mediaPlayer: MediaPlayer? = null

private fun initAudio() {
    Log.v(LOG_TAG, "initAudio")

    audioManager ?: let {
        audioManager = context!!.getSystemService(Context.AUDIO_SERVICE) as AudioManager?
    }

    mediaPlayer = try {
        MediaPlayer().apply {
            if (Build.VERSION.SDK_INT >= 21) {
                val audioAttributes = AudioAttributes.Builder()
                    .setUsage(AudioAttributes.USAGE_MEDIA)
                    .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
                    .build()
                setAudioAttributes(audioAttributes)
            } else {
                setAudioStreamType(AudioManager.STREAM_MUSIC)
            }
            if (Build.VERSION.SDK_INT <= 28) {
                setDataSource(context!!, Uri.parse("file:///system/media/audio/ui/camera_click.ogg"))
            } else {
                setDataSource(context!!, Uri.parse("file:///system/product/media/audio/ui/camera_click.ogg"))
            }
            prepare()
        }
    } catch (e: Exception) {
        Log.e(LOG_TAG, "initAudio", e)
        null
    }
}

private fun playClickSound() {

    if (audioManager?.ringerMode == AudioManager.RINGER_MODE_NORMAL) {
        mediaPlayer?.start()
    }
}

0
        AudioManager meng = (AudioManager) getContext().getSystemService(Context.AUDIO_SERVICE);
        int volume = meng.getStreamVolume( AudioManager.STREAM_NOTIFICATION);
        if(volume != 0)
            sound.play(MediaActionSound.SHUTTER_CLICK);

3
尽管这段代码可能解决问题,但一个好的答案也应该解释代码做了什么以及它如何帮助解决问题。 - BDL

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接