Android 9系统源码_音频管理(一)按键音效源码解析

news2024/10/6 12:23:04

前言

当用户点击Android智能设备的按钮的时候,如果伴随有按键音效的话,会给用户更好的交互体验。本期我们将会结合Android系统源码来具体分析一下控件是如何发出按键音效的。

一、系统加载按键音效资源

1、在TV版的Android智能设备中,我们可以通过调节设置页面的开关来控制按键音效的有无,该设置页面对应的系统源码如下所示。

packages/apps/TvSettings/Settings/src/com/android/tv/settings/device/sound/SoundFragment.java

public class SoundFragment extends PreferenceControllerFragment implements
        Preference.OnPreferenceChangeListener {

    private AudioManager mAudioManager;
    private Map<Integer, Boolean> mFormats;

    public static SoundFragment newInstance() {
        return new SoundFragment();
    }

    @Override
    public void onAttach(Context context) {
        mAudioManager = context.getSystemService(AudioManager.class);
        mFormats = mAudioManager.getSurroundFormats();
        super.onAttach(context);
    }
    
	//用户的点击行为首先触发此方法
    @Override
    public boolean onPreferenceTreeClick(Preference preference) {
        if (TextUtils.equals(preference.getKey(), KEY_SOUND_EFFECTS)) {
            final TwoStatePreference soundPref = (TwoStatePreference) preference;
            //调用setSoundEffectsEnabled来设置按键音的开启与关闭
            setSoundEffectsEnabled(soundPref.isChecked());
        }
        return super.onPreferenceTreeClick(preference);
    }

	//获取按键音效是否开启
    public static boolean getSoundEffectsEnabled(ContentResolver contentResolver) {
        return Settings.System.getInt(contentResolver, Settings.System.SOUND_EFFECTS_ENABLED, 1)
                != 0;
    }

	//设置是否开启按键音效
    private void setSoundEffectsEnabled(boolean enabled) {
        if (enabled) {
        	//如果开启按键音,则调用AudioManager的loadSoundEffects方法来加载按键音效资源
            mAudioManager.loadSoundEffects();
        } else {
            mAudioManager.unloadSoundEffects();
        }
        Settings.System.putInt(getActivity().getContentResolver(),
                Settings.System.SOUND_EFFECTS_ENABLED, enabled ? 1 : 0);
    }

}

我们在设置页面点击按键音效开关按钮,最终会触发SoundFragment的setSoundEffectsEnabled方法,该方法会判断是否开启按键音,如果开启,则调用AudioManager的loadSoundEffects方法来加载按键音效资源,反之则会调用unloadSoundEffects方法不加载音效资源。

2、AudioManager的loadSoundEffects方法如下所示。

frameworks/base/media/java/android/media/AudioManager.java

public class AudioManager {

    //获取AudioService的代理对象
    private static IAudioService getService()
    {
        if (sService != null) {
            return sService;
        }
        IBinder b = ServiceManager.getService(Context.AUDIO_SERVICE);
        sService = IAudioService.Stub.asInterface(b);
        return sService;
    }

    public void loadSoundEffects() {
        final IAudioService service = getService();
        try {
        	//调用AudioService服务的loadSoundEffects方法
            service.loadSoundEffects();
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }
    
    public void unloadSoundEffects() {
        final IAudioService service = getService();
        try {
            service.unloadSoundEffects();
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }
}

AudioManager的首先通过getService方法获取了音频服务AudioService的代理对象,然后调用该对象的具体方法。

3、AudioService的loadSoundEffects方法如下所示。

public class AudioService extends IAudioService.Stub
        implements AccessibilityManager.TouchExplorationStateChangeListener,
            AccessibilityManager.AccessibilityServicesStateChangeListener {
       
     private AudioHandler mAudioHandler;

	 //加载音效资源
     public boolean loadSoundEffects() {
        int attempts = 3;
        LoadSoundEffectReply reply = new LoadSoundEffectReply();

        synchronized (reply) {
        	//调用sendMsg发送消息给mAudioHandler。
            sendMsg(mAudioHandler, MSG_LOAD_SOUND_EFFECTS, SENDMSG_QUEUE, 0, 0, reply, 0);
            while ((reply.mStatus == 1) && (attempts-- > 0)) {
                try {
                    reply.wait(SOUND_EFFECTS_LOAD_TIMEOUT_MS);
                } catch (InterruptedException e) {
                    Log.w(TAG, "loadSoundEffects Interrupted while waiting sound pool loaded.");
                }
            }
        }
        return (reply.mStatus == 0);
    }
    
    //不加载音效资源
    public void unloadSoundEffects() {
        sendMsg(mAudioHandler, MSG_UNLOAD_SOUND_EFFECTS, SENDMSG_QUEUE, 0, 0, null, 0);
    }

    private static void sendMsg(Handler handler, int msg,
            int existingMsgPolicy, int arg1, int arg2, Object obj, int delay) {

        if (existingMsgPolicy == SENDMSG_REPLACE) {
            handler.removeMessages(msg);
        } else if (existingMsgPolicy == SENDMSG_NOOP && handler.hasMessages(msg)) {
            return;
        }
        synchronized (mLastDeviceConnectMsgTime) {
            long time = SystemClock.uptimeMillis() + delay;

            if (msg == MSG_SET_A2DP_SRC_CONNECTION_STATE ||
                msg == MSG_SET_A2DP_SINK_CONNECTION_STATE ||
                msg == MSG_SET_HEARING_AID_CONNECTION_STATE ||
                msg == MSG_SET_WIRED_DEVICE_CONNECTION_STATE ||
                msg == MSG_A2DP_DEVICE_CONFIG_CHANGE ||
                msg == MSG_BTA2DP_DOCK_TIMEOUT) {
                if (mLastDeviceConnectMsgTime >= time) {
                  // add a little delay to make sure messages are ordered as expected
                  time = mLastDeviceConnectMsgTime + 30;
                }
                mLastDeviceConnectMsgTime = time;
            }
			//调用handler的sendMessageAtTime方法
            handler.sendMessageAtTime(handler.obtainMessage(msg, arg1, arg2, obj), time);
        }
    }
    
    private class AudioHandler extends Handler {
    		//加载音效
            private boolean onLoadSoundEffects() {
            	...代码暂时省略...
            }
            @Override
        	public void handleMessage(Message msg) {
            switch (msg.what) {
            	...代码省略...
            	 case MSG_UNLOAD_SOUND_EFFECTS:
                    onUnloadSoundEffects();//不加载音效
                    break;
            	 case MSG_LOAD_SOUND_EFFECTS://加载音效
                    boolean loaded = onLoadSoundEffects();//调用onLoadSoundEffects加载音效,并将加载结果赋值给loaded
                    if (msg.obj != null) {
                        LoadSoundEffectReply reply = (LoadSoundEffectReply)msg.obj;
                        synchronized (reply) {
                            reply.mStatus = loaded ? 0 : -1;
                            reply.notify();
                        }
                    }
                    break;
              	...代码省略...
            }
    }
}

这里我们只要分析一下AudioService的加载音效资源的loadSoundEffects方法,该方法会调用sendMsg,发送类型为MSG_UNLOAD_SOUND_EFFECTS的msg给mAudioHandler。然后会进一步触发AudioHandler的handleMessage方法,该消息最终会触发onLoadSoundEffects方法。

4、AudioHandler的onLoadSoundEffects方法如下所示。

public class AudioService extends IAudioService.Stub
        implements AccessibilityManager.TouchExplorationStateChangeListener,
            AccessibilityManager.AccessibilityServicesStateChangeListener {
    //音效资源文件名称
    private static final List<String> SOUND_EFFECT_FILES = new ArrayList<String>();
    
    private class AudioHandler extends Handler {
		//加载音效
        private boolean onLoadSoundEffects() {
            int status;
            synchronized (mSoundEffectsLock) {
            	//如果系统未启动完毕直接返回
                if (!mSystemReady) {
                    Log.w(TAG, "onLoadSoundEffects() called before boot complete");
                    return false;
                }
                //如果mSoundPool不为空直接返回
                if (mSoundPool != null) {
                    return true;
                }
                //加载触摸音效
                loadTouchSoundAssets();
   				...代码暂时省略...
        }
        
    //加载按键音效资源
    private void loadTouchSoundAssets() {
        XmlResourceParser parser = null;
        //如果音效资源文件列表不为空直接返回
        if (!SOUND_EFFECT_FILES.isEmpty()) {
            return;
        }
        //加载按键默认的音效资源
        loadTouchSoundAssetDefaults();
       ...代码省略...
	}
	
    private void loadTouchSoundAssetDefaults() {
    	//在类型为List<String>的SOUND_EFFECT_FILES中添加默认的按键音效资源Effect_Tick.ogg
        SOUND_EFFECT_FILES.add("Effect_Tick.ogg");
        for (int i = 0; i < AudioManager.NUM_SOUND_EFFECTS; i++) {
            SOUND_EFFECT_FILES_MAP[i][0] = 0;
            SOUND_EFFECT_FILES_MAP[i][1] = -1;
        }
    }
}

onLoadSoundEffects方法首先判断系统是否已经启动完毕,如果未启动直接返回;然后判断mSoundPool是否空,如果不为空则直接返回;
然后首先会调用一个关键方法loadTouchSoundAssets,该方法首先判断音效资源文件列表SOUND_EFFECT_FILES是否为空,不为空直接返回。如果以上判断都不成立,则会调用loadTouchSoundAssetDefaults方法加载按键默认的音效资源,该方法首先在SOUND_EFFECT_FILES的添加音效资源Effect_Tick.ogg。

5、继续往下看AudioHandler的onLoadSoundEffects方法。

public class AudioService extends IAudioService.Stub
        implements AccessibilityManager.TouchExplorationStateChangeListener,
            AccessibilityManager.AccessibilityServicesStateChangeListener {
    //音效资源文件名称
    private static final List<String> SOUND_EFFECT_FILES = new ArrayList<String>();
    
    private class AudioHandler extends Handler {
		//加载音效
        private boolean onLoadSoundEffects() {
            int status;

            synchronized (mSoundEffectsLock) {
                if (!mSystemReady) {
                    Log.w(TAG, "onLoadSoundEffects() called before boot complete");
                    return false;
                }

                if (mSoundPool != null) {
                    return true;
                }
                //加载触摸音效
                loadTouchSoundAssets();
                //创建SoundPool对象
                mSoundPool = new SoundPool.Builder()
                        .setMaxStreams(NUM_SOUNDPOOL_CHANNELS)
                        .setAudioAttributes(new AudioAttributes.Builder()
                            .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION)
                            .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
                            .build())
                        .build();
         		...代码省略...
                int numSamples = 0;
                for (int effect = 0; effect < AudioManager.NUM_SOUND_EFFECTS; effect++) {
                    // Do not load sample if this effect uses the MediaPlayer
                    if (SOUND_EFFECT_FILES_MAP[effect][1] == 0) {
                        continue;
                    }
                    if (poolId[SOUND_EFFECT_FILES_MAP[effect][0]] == -1) {
                        //获取音效资源文件路径
                        String filePath = getSoundEffectFilePath(effect);
                        //使用SoundPool加载音效资源文件
                        int sampleId = mSoundPool.load(filePath, 0);
                        if (sampleId <= 0) {
                            Log.w(TAG, "Soundpool could not load file: "+filePath);
                        } else {
                            SOUND_EFFECT_FILES_MAP[effect][1] = sampleId;
                            poolId[SOUND_EFFECT_FILES_MAP[effect][0]] = sampleId;
                            numSamples++;
                        }
                    } else {
                        SOUND_EFFECT_FILES_MAP[effect][1] =
                                poolId[SOUND_EFFECT_FILES_MAP[effect][0]];
                    }
                }
			...代码省略...
        }
        
        //获取音效资源文件路径,默认返回的音效资源文件路径为/system/media/audio/ui/Effect_Tick.ogg
        private String getSoundEffectFilePath(int effectType) {
            //  /product + /media/audio/ui/ + Effect_Tick.ogg
            String filePath = Environment.getProductDirectory() + SOUND_EFFECTS_PATH
                    + SOUND_EFFECT_FILES.get(SOUND_EFFECT_FILES_MAP[effectType][0]);
            if (!new File(filePath).isFile()) {
                //   /system + /media/audio/ui/ + Effect_Tick.ogg
                filePath = Environment.getRootDirectory() + SOUND_EFFECTS_PATH
                        + SOUND_EFFECT_FILES.get(SOUND_EFFECT_FILES_MAP[effectType][0]);
            }
            return filePath;
        }
 	 }
 }

onLoadSoundEffects先是调用loadTouchSoundAssets方法加载默认的音效资源文件名称,然后构建SoundPool实例对象,随后调用getSoundEffectFilePath获取按键音效资源文件路径,默认返回的音效资源文件路径为/system/media/audio/ui/Effect_Tick.ogg,并调用SoundPool加载该音效资源。

6、简单回顾一下以上步骤。
系统加载按键音效资源

二、点击控件,播放音效资源

在系统开启按键音效之后,当我们点击任意控件之后,都会触发按键音效。接下来我们将会结合View的系统源码来梳理该流程。

1、当我们点击一个控件的时候,首先会触发该View的performClick方法。

frameworks/base/core/java/android/view/View.java

public class View implements Drawable.Callback, KeyEvent.Callback,
        AccessibilityEventSource {
	
    public boolean performClick() {
        // We still need to call this method to handle the cases where performClick() was called
        // externally, instead of through performClickInternal()
        notifyAutofillManagerOnClick();

        final boolean result;
        final ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnClickListener != null) {
            playSoundEffect(SoundEffectConstants.CLICK);//播放按键点击音效
            li.mOnClickListener.onClick(this);
            result = true;
        } else {
            result = false;
        }

        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
        notifyEnterOrExitForAutoFillIfNeeded(true);

        return result;
    }
    
    public void playSoundEffect(int soundConstant) {
        //判断mAttachInfo.mRootCallbacks是否为空,以及系统是否开启了按键音效
        if (mAttachInfo == null || mAttachInfo.mRootCallbacks == null || !isSoundEffectsEnabled()) {
            return;
        }
        //调用mAttachInfo.mRootCallbacks的playSoundEffect方法
        mAttachInfo.mRootCallbacks.playSoundEffect(soundConstant);
    }
 }

View的performClick方法会调用playSoundEffect方法,playSoundEffect方法首先判断mAttachInfo.mRootCallbacks是否为空,以及系统是否开启了按键音效,然后调用mAttachInfo.mRootCallbacks的playSoundEffect方法。我们知道WindowManager在将View添加到窗口的过程中,都需要用到ViewRootImpl这个类,具体请参考Android 9.0系统源码_窗口管理(二)WindowManager对窗口的管理过程这篇文章。

2、mAttachInfo最初是在ViewRootImpl的构造方法中被创建的。

frameworks/base/core/java/android/view/ViewRootImpl.java

public final class ViewRootImpl implements ViewParent,
        View.AttachInfo.Callbacks, ThreadedRenderer.DrawCallbacks {
        
    public ViewRootImpl(Context context, Display display) {
        mContext = context;
        mWindowSession = WindowManagerGlobal.getWindowSession();
        mDisplay = display;
        mBasePackageName = context.getBasePackageName();
        mThread = Thread.currentThread();
        mLocation = new WindowLeaked(null);
        mLocation.fillInStackTrace();
        mWidth = -1;
        mHeight = -1;
        mDirty = new Rect();
        mTempRect = new Rect();
        mVisRect = new Rect();
        mWinFrame = new Rect();
        mWindow = new W(this);
        mTargetSdkVersion = context.getApplicationInfo().targetSdkVersion;
        mViewVisibility = View.GONE;
        mTransparentRegion = new Region();
        mPreviousTransparentRegion = new Region();
        mFirst = true; // true for the first time the view is added
        mAdded = false;
        //创建AttachInfo对象,倒数第二个参数就是View的playSoundEffect方法所用到的回调对象
        mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this, context);
		...代码省略...
    }
}

3、看完ViewRootImpl的构造方法,再来看下AttachInfo的构造方法。

public class View implements Drawable.Callback, KeyEvent.Callback,
        AccessibilityEventSource {
        
    final static class AttachInfo {

		//关键回调接口
        interface Callbacks {
        	//播放音效
            void playSoundEffect(int effectId);
            boolean performHapticFeedback(int effectId, boolean always);
        }

        final Callbacks mRootCallbacks;

        AttachInfo(IWindowSession session, IWindow window, Display display,
                ViewRootImpl viewRootImpl, Handler handler, Callbacks effectPlayer,
                Context context) {
            mSession = session;
            mWindow = window;
            mWindowToken = window.asBinder();
            mDisplay = display;
            mViewRootImpl = viewRootImpl;
            mHandler = handler;
            mRootCallbacks = effectPlayer;//View的playSoundEffect方法所用到的回调对象就是这个
            mTreeObserver = new ViewTreeObserver(context);
        }
    }
 }

AttachInfo构造方法的最后一个参数很关键,因为View的playSoundEffect方法所调用的对象就是这个,结合ViewRootImpl的代码我们可以知道是ViewRootImpl实现了这个回调。

4、ViewRootImpl的playSoundEffect方法如下所示。

public final class ViewRootImpl implements ViewParent, View.AttachInfo.Callbacks, ThreadedRenderer.DrawCallbacks {
        
    public void playSoundEffect(int effectId) {
        checkThread();//检测是否是UI线程
        try {
            final AudioManager audioManager = getAudioManager();

            switch (effectId) {
                case SoundEffectConstants.CLICK://播放按键点击音效
                    audioManager.playSoundEffect(AudioManager.FX_KEY_CLICK);
                    return;
                case SoundEffectConstants.NAVIGATION_DOWN:
                    audioManager.playSoundEffect(AudioManager.FX_FOCUS_NAVIGATION_DOWN);
                    return;
                case SoundEffectConstants.NAVIGATION_LEFT:
                    audioManager.playSoundEffect(AudioManager.FX_FOCUS_NAVIGATION_LEFT);
                    return;
                case SoundEffectConstants.NAVIGATION_RIGHT:
                    audioManager.playSoundEffect(AudioManager.FX_FOCUS_NAVIGATION_RIGHT);
                    return;
                case SoundEffectConstants.NAVIGATION_UP:
                    audioManager.playSoundEffect(AudioManager.FX_FOCUS_NAVIGATION_UP);
                    return;
                default:
                    throw new IllegalArgumentException("unknown effect id " + effectId +
                            " not defined in " + SoundEffectConstants.class.getCanonicalName());
            }
        } catch (IllegalStateException e) {
            // Exception thrown by getAudioManager() when mView is null
            Log.e(mTag, "FATAL EXCEPTION when attempting to play sound effect: " + e);
            e.printStackTrace();
        }
    }
}

ViewRootImpl的playSoundEffect方法首先会检测一下当前线程是不是UI线程,然后会根据传入的effectId类型来判断要播放那种音效。因为View的performClick方法传入的是SoundEffectConstants.CLICK,所以会触发audioManager.playSoundEffect(AudioManager.FX_KEY_CLICK)。

4、AudioManager的playSoundEffect方法如下所示。

public class AudioManager {
    public void  playSoundEffect(int effectType) {
        //检测音效类型是否合规
        if (effectType < 0 || effectType >= NUM_SOUND_EFFECTS) {
            return;
        }
        //确定音效是否可用
        if (!querySoundEffectsEnabled(Process.myUserHandle().getIdentifier())) {
            return;
        }
        //获取AudioService服务
        final IAudioService service = getService();
        try {
            //调用服务的playSoundEffect方法
            service.playSoundEffect(effectType);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }
    
    /**
     * Settings has an in memory cache, so this is fast.
     */
    private boolean querySoundEffectsEnabled(int user) {
        return Settings.System.getIntForUser(getContext().getContentResolver(),
                Settings.System.SOUND_EFFECTS_ENABLED, 0, user) != 0;
    }
}

AudioManager的playSoundEffect会做一些校验,如果校验通过则会获取AudioService服务对象,并调用该对象的playSoundEffect方法进行音效播放。

5、AudioService和playSoundEffect相关的代码如下所示。

public class AudioService extends IAudioService.Stub
        implements AccessibilityManager.TouchExplorationStateChangeListener,
            AccessibilityManager.AccessibilityServicesStateChangeListener {
            
    /**
     * 播放音效
     * @param effectType
     */
    public void playSoundEffect(int effectType) {
        playSoundEffectVolume(effectType, -1.0f);
    }

    public void playSoundEffectVolume(int effectType, float volume) {
        // do not try to play the sound effect if the system stream is muted
        if (isStreamMutedByRingerOrZenMode(STREAM_SYSTEM)) {
            return;
        }

        if (effectType >= AudioManager.NUM_SOUND_EFFECTS || effectType < 0) {
            Log.w(TAG, "AudioService effectType value " + effectType + " out of range");
            return;
        }
		//发送播放音效的消息给mAudioHandler
        sendMsg(mAudioHandler, MSG_PLAY_SOUND_EFFECT, SENDMSG_QUEUE,
                effectType, (int) (volume * 1000), null, 0);
    }
    
    private class AudioHandler extends Handler {
    		//加载音效
            private boolean onLoadSoundEffects() {
            	...代码暂时省略...
            }
            @Override
        	public void handleMessage(Message msg) {
            switch (msg.what) {
            	...代码省略...
            	 case MSG_UNLOAD_SOUND_EFFECTS:
                    onUnloadSoundEffects();//不加载音效
                    break;
            	 case MSG_LOAD_SOUND_EFFECTS://加载音效
	               boolean loaded = onLoadSoundEffects();//调用onLoadSoundEffects加载音效,并将加载结果赋值给loaded
            		...代码省略...
                    break;
                case MSG_PLAY_SOUND_EFFECT://播放音效
                    onPlaySoundEffect(msg.arg1, msg.arg2);
                    break;
              	...代码省略...
            }
    }
}

AudioService的playSoundEffect方法进一步调用playSoundEffectVolume,该方法会发送播放音效的消息MSG_PLAY_SOUND_EFFECT给mAudioHandler,最终会触发onPlaySoundEffect方法。

6、AudioService的onPlaySoundEffec方法如下所示。

public class AudioService extends IAudioService.Stub
        implements AccessibilityManager.TouchExplorationStateChangeListener,
            AccessibilityManager.AccessibilityServicesStateChangeListener {
            
        private void onPlaySoundEffect(int effectType, int volume) {
            synchronized (mSoundEffectsLock) {

                onLoadSoundEffects();//加载音效

                if (mSoundPool == null) {
                    return;
                }
                float volFloat;
                // use default if volume is not specified by caller
                if (volume < 0) {
                    volFloat = (float)Math.pow(10, (float)sSoundEffectVolumeDb/20);
                } else {
                    volFloat = volume / 1000.0f;
                }

                if (SOUND_EFFECT_FILES_MAP[effectType][1] > 0) {
                	//通过SoundPool播放音效
                    mSoundPool.play(SOUND_EFFECT_FILES_MAP[effectType][1],
                                        volFloat, volFloat, 0, 0, 1.0f);
                } else {
                    //通过MediaPlayer播放音效
                    MediaPlayer mediaPlayer = new MediaPlayer();
                    try {
                        String filePath = getSoundEffectFilePath(effectType);
                        mediaPlayer.setDataSource(filePath);
                        mediaPlayer.setAudioStreamType(AudioSystem.STREAM_SYSTEM);
                        mediaPlayer.prepare();
                        mediaPlayer.setVolume(volFloat);
                        mediaPlayer.setOnCompletionListener(new OnCompletionListener() {
                            public void onCompletion(MediaPlayer mp) {
                                cleanupPlayer(mp);
                            }
                        });
                        mediaPlayer.setOnErrorListener(new OnErrorListener() {
                            public boolean onError(MediaPlayer mp, int what, int extra) {
                                cleanupPlayer(mp);
                                return true;
                            }
                        });
                        mediaPlayer.start();
                    } catch (IOException ex) {
                        Log.w(TAG, "MediaPlayer IOException: "+ex);
                    } catch (IllegalArgumentException ex) {
                        Log.w(TAG, "MediaPlayer IllegalArgumentException: "+ex);
                    } catch (IllegalStateException ex) {
                        Log.w(TAG, "MediaPlayer IllegalStateException: "+ex);
                    }
                }
            }
        }
   }

7、简单回顾一下以上步骤。
点击控件,播放音效资源

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/820452.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

vscode插件美化C/C++和HTML

配置html语言自动补全&#xff0c;在设置中搜索Emmet 设置->搜索 Emmet Abbreviation 验证自动补全 输入html&#xff0c;下键选择html:5&#xff0c;自动补全整个开头语言

极简在线商城系统,支持docker一键部署

Hmart 给大家推荐一个简约自适应电子商城系统&#xff0c;针对虚拟商品在线发货&#xff0c;支持企业微信通知&#xff0c;支持docker一键部署&#xff0c;个人资质也可搭建。 前端 后端 H2 console 运行命令 docker run -d --name mall --restartalways -p 8080:8080 -e co…

Maven引入本地第三方Jar包

背景 引入第三方的Jar包&#xff0c;本来是在项目结构的库里面引入的jar包&#xff0c;但是用maven打包时报错&#xff0c;所以后面想着用maven引入本地地址的方式。 解决方法 1.将Jar包放到根目录里面&#xff0c;我这边放在了lib文件中,如图所示&#xff1a; 2.在pom文件…

深空物联网通信中视频流的智能多路TCP拥塞控制|文献阅读|文献分析和学习|拥塞控制|MPTCP|SVC

前言 那么这里博主先安利一些干货满满的专栏了&#xff01; 首先是博主的高质量博客的汇总&#xff0c;这个专栏里面的博客&#xff0c;都是博主最最用心写的一部分&#xff0c;干货满满&#xff0c;希望对大家有帮助。 高质量博客汇总https://blog.csdn.net/yu_cblog/categ…

【CAS6.6源码解析】ST的默认淘汰策略 MultiTimeUseOrTimeoutExpirationPolicy

本篇文章是对上篇文章【深度解析票据淘汰与过期策略】的一个补充&#xff0c;上篇文章主要分析了TGT的默认淘汰策略配置&#xff0c;ST的配置有TGT的默认配置有一些差异化&#xff0c;特别是ST是基于MultiTimeUseOrTimeoutExpirationPolicy这样一种淘汰策略&#xff0c;本文将详…

精密划片机行业发展趋势

划片机行业的发展趋势主要包括以下几个方面&#xff1a; 高精度、高效率的切割技术&#xff1a;随着半导体芯片的尺寸不断增大&#xff0c;切割的精度和效率要求也越来越高。因此&#xff0c;行业将继续推动切割技术的创新和发展&#xff0c;以提高划片效率和精度。 自动化和智…

百度地图点标记加调用

先看效果 PHP代码 <?phpnamespace kds_addons\edata\controller;use think\addons\Controller; use think\Db;class Maps extends Controller {// 经纬度计算面积function calculate_area($points){$totalArea 0;$numPoints count($points);if ($numPoints > 2) {f…

Blazor前后端框架Known-V1.2.9

V1.2.9 Known是基于C#和Blazor开发的前后端分离快速开发框架&#xff0c;开箱即用&#xff0c;跨平台&#xff0c;一处代码&#xff0c;多处运行。 Gitee&#xff1a; https://gitee.com/known/KnownGithub&#xff1a;https://github.com/known/Known 概述 基于C#和Blazor…

(学习笔记1)类和对象详解(C++)

类和对象 1.类的定义语法类的定义方式 2.类的访问限定符2.1 访问限定符的特点2.2 struct与class的区别 3. 三大特性3.1 封装4.类的作用域5.类的实例化6.类对象模型7.this指针7.1 this指针的特性7.2 this指针相关问题 1.类的定义 类是面向对象语言特有的语法&#xff0c;在面向…

数组中重复的数字_剑指 Offer 03

文章目录 题目描述法一 哈希表 题目描述 法一 哈希表 int findRepeatNumber(vector<int>& nums){unordered_map<int, bool> map;for(const int& num:nums){if(map[num]) return num;map[num]true;}return -1;}

c++游戏制作指南 模板(权当预告)

&#x1f37f;*★,*:.☆(&#xffe3;▽&#xffe3;)/$:*.★* &#x1f37f; &#x1f35f;欢迎来到静渊隐者的csdn博文&#xff0c;本文是c游戏制作指南的一部&#x1f35f; &#x1f355;更多文章请点击下方链接&#x1f355; &#x1f368; c游戏制作指南&#x1f3…

C++调用C# dll成功示例

方法1&#xff1a;公共语言运行时支持 一.准备C# dll类库 。生成CSLib.dll namespace CSLib {public class Class1{private string name;public string Name{get{return name;}set{name "Your Name: " value;}}} }二、写C应用调用 1)需要把dll复制到运行目录下。…

Python实现GA遗传算法优化BP神经网络回归模型(BP神经网络回归算法)项目实战

说明&#xff1a;这是一个机器学习实战项目&#xff08;附带数据代码文档视频讲解&#xff09;&#xff0c;如需数据代码文档视频讲解可以直接到文章最后获取。 1.项目背景 遗传算法&#xff08;Genetic Algorithm&#xff0c;GA&#xff09;最早是由美国的 John holland于20世…

超细详解,接口自动化测试-JSON和JsonPath提取数据(实战)

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 JSON(JavaScript …

修改若依框架为自己的项目并上传到git

第一步: 打开后台若依项目,把全局替换打开,搜索ruoyi 和 RuoYi 和 若依 分别换成自己公司的名称( 记住要把区分大小写打开 ) 第二步: 关闭idea中的项目,然后在文件夹中打开这个项目,然后搜索target( 缓冲 ) 删除,部分人的电脑上面还有imp文件切记也要删除 第三步: 接着把项目…

Day09-作业(Mybatis)

作业1&#xff1a;通过SpringBootMybatis的方式完成如下学员管理的需求 页面原型&#xff1a; 数据准备&#xff1a; -- 学员表 create table student(id int unsigned primary key auto_increment comment ID,主键,name varchar(10) not null comment 姓名,no char(10) not …

C++多线程基本原理详解

在C学习过程中&#xff0c;要想“更上一层楼”的话&#xff0c;多线程编程是必不可少的一步&#xff0c;大家需要更多的思考是为什么这么做&#xff1f;这样做的好处是什么&#xff1f;以及多线程编程都可以应用在哪里&#xff1f; 1、多线程 传统的C&#xff08;C11标准之前…

R语言中数据重塑(长宽表的转化)

学习笔记&#xff0c;仅供学习使用。 目录 1-什么是整洁的数据&#xff1f; 2-宽表变成长表 示例1&#xff1a; 示例2&#xff1a; 示例3&#xff1a; 3-长表变宽表 示例1&#xff1a; 示例2&#xff1a; 1-什么是整洁的数据&#xff1f; 按照Hadley的表述&#xf…

我的观影记录表【个人向】

目录 前言电影评分标准闪电侠&#xff08;2023&#xff09;银河护卫队3&#xff08;2023&#xff09; 前言 这里是我本人的观影记录&#xff0c;这个想法2年前就有了&#xff0c;但是一直比较懒&#xff0c;现在&#xff08;上班摸鱼&#xff09;准备重新开始&#xff0c;评价…

Spring Boot Starter 剖析与实践 | 京东云技术团队

引言 对于 Java 开发人员来说&#xff0c;Spring 框架几乎是必不可少的。它是一个广泛用于开发企业应用程序的开源轻量级框架。近几年&#xff0c;Spring Boot 在传统 Spring 框架的基础上应运而生&#xff0c;不仅提供了 Spring 的全部功能&#xff0c;还使开发人员更加便捷地…