当前位置:   article > 正文

本科生尝试安卓APP开发:手机调音器节拍器(音乐学习者使用)_调音器节拍器源码

调音器节拍器源码

目录

写在前面  

本项目的灵感来源

作品完成度

项目介绍

APP的名字:沐尘调音器

APP功能介绍 

调音器

节拍器

实际测试

调音器测试 

节拍器测试 

项目内容

开发环境

APP功能原理

1.调音器-编程原理

2.节拍器-编程原理

代码框架

1.代码文件的分类

2.Manifest配置文件

3.启动界面和主界面

4.调音器界面

5.节拍器界面

项目总结

遇到的问题

总结


写在前面  

本项目的灵感来源

本人二胡练习时长11年半,本科也在校民乐团待了4年。众所周知,学习弦乐器、管乐器、部分打击乐器,包括二胡、古筝、琵琶、中阮、提琴、钢琴、吉他、竖琴、竹笛、马林巴等,必然会接触到调音器(用来校准乐器的音准)。另外,所有学习音乐的人群都会接触到节拍器(用来帮助练习节奏准确度)

然而,当我发现自己经常用的这种APP有广告而且开始收费时,我就想,能不能自己做一个这种类型的APP呢,正好自己也经常能用的到?作为一个在学校系统学过C语言和Python,自学过一些C++、C#的自动化工科生,我就这样开始了大胆的尝试。

作品完成度

如今,经过大三近2个月的努力,加上毕业这段时间对APP进行了Bug修复,我最终写完了这个APP ,并写下这篇近2万字的博客,作为我的“第二个本科毕业设计”的一个纪念。

本作品基本上实现了所有预定的功能,算是一次比较成功的尝试(对于一个刚开始对安卓开发一无所知的人来说,已经超级成功了hhh)。本项目是本人在大三寒假时开始做的,做了2个月左右,包括前期的学习和搜集资料,以及最后软件bug的修复。因为近一年比较忙,这个APP大体上完成一年多之后才来写这个博客。趁着毕业季,花了点时间修复了APP的bug,完成这篇博客,作为一个项目总结和经验分享~

需要声明的是,APP的UI部分(界面、按钮以及动画)完全是纯自己原创的,包括插图;调音器的数字信号处理部分有参考GitHub上的开源项目,做了一些修改;节拍器的代码框架也是有参考和学习GitHub国外的开源项目。(地址在结尾)

项目介绍

APP的名字:沐尘调音器

本APP是音乐类Android应用程序,主要提供调音器节拍器功能。

开发该APP学习到的能力:Android开发的基本知识(包括Android系统基本架构、线程、接口等等)、十二平均律音乐相关知识、使用麦克风接收声音产生音频信号数字信号处理应用层信息传输自定义接口、自定义线程媒体播放、闪光灯调用、手机振动调用、可视化动画UI设计、插图设计等

本APP有原创的LOGO和UI界面,APP内包含启动界面,菜单界面,调音器界面,节拍器界面(包含三个fragment和底部菜单栏) 。用户点击图标进入APP后,会进入启动界面并保持2秒,之后自动跳转到菜单界面。菜单界面有两个按钮,分别对应调音器和节拍器。

LOGO图标
启动界面
菜单界面

APP功能介绍 

调音器

具有十二平均律调音功能,默认以440Hz为标准音,用户也可以点击按钮切换标准音为442Hz。调音器界面包含调音仪表盘、当前声音的音名、当前声音与标准音准的偏差音分数,以及一个显示当前声音音准对应位置的钢琴键盘。

进入调音器界面程序会自动请求调用手机麦克风的权限,进行收音,同时显示出当前声音的音名和音准度数。背景色块显示蓝色表示音准比较标准,在±10音分之内;显示红色表示音准不太准,需要进一步调整。校音范围是固定音C0~B8,每个八度12个半音都能识别出来。用户可以根据显示的信息,自行调整声音的音准,实现帮助用户调整音准的功能。

沐尘调音器-调音器部分

节拍器

用户可以点击界面中的按钮调整参数,点击播放按钮可以实现节拍器声音的播放和暂停。当节拍器播放时,界面中的节拍器图像会像现实中的机械节拍器一样,进行不同速度地自然摆动,同时手机会遵循节奏播放节拍器声音。在修改速度、重音、节奏型的同时,节拍器声音的播放也会同步调整(强调即时地、同步地响应参数修改的效果)

用户还可以由底部菜单栏,切换三个界面:“节拍器”、“更多、“设置””(中间的“更多”界面暂时是空白的,还没有想好要做什么内容hhh)

在设置界面中,可以开启闪光灯模式,即手机闪光灯会随节拍器的节奏闪烁;可以开启振动模式,即手机会随节拍器的节奏进行振动。还可以选择切换节拍器的音色。

节拍器界面
设置界面

沐尘调音器-节拍器部分

本APP的安装包下载链接: 

链接:https://pan.baidu.com/s/10XFGE2-W4JnOfNzYunOJnA 
提取码:3vef 

注:由于懒得把UI弄成适应所有手机屏幕大小了,所以不一定能适应非全面屏的手机屏幕,若适应不了你的手机屏幕就将就着用下吧(=v=)

实际测试

实际测试的视频发在了b站: 第二个本科毕业设计|自制调音器节拍器APP_哔哩哔哩_bilibili

调音器测试 

与实体电子调音器对比,本APP音高检测的灵敏度上要更好一些。

节拍器测试 

速度变换、节奏型变换、重音开关、音色切换、手电筒、振动功能都正常。 

项目内容

主要是分享一些功能原理、代码框架,并分享一下我从刚开始学习Android开发和Java相关语法,一直到最后开发出APP的大致经历、遇到的困难以及解决方法。本博客是纯经验分享(非教学),有错误可以在评论区补充~

开发环境

开发软件:Android Studio

开发语言:Java

插图和图标绘制软件:Adobe Illustrator 2019

SDK版本:

  • compileSDK版本:32
  • minSDK版本:21
  • targetSDK版本:32
  • JDK 版本:1.8

 Android Studio、gradle和Sdk版本更新到最新版就行

APP功能原理

1.调音器-编程原理

  • 调音器程序执行的大致流程:

经过计算,最终,调音器界面UI要显示的信息主要是2个:距离当前声音音高最近的标准音符、当前声音与之的音分偏差

  •  快速傅里叶变换与YIN算法:

        FFT(Fast Fourier Transmit),快速傅里叶变换,可以将时域转化为频域。FFT是离散傅氏变换(DFT) 的快速算法。

        本项目使用了TarsosDSP库的音高估计算法FFT.YIN,其从每一帧信号FFT中获得的功率谱中提取基频,将峰值频率确定为当前声音的频率。

  • 音乐理论——十二平均律:

        十二平均律将一个八度分为12个音,每个音之间间隔一个半音音高

        该APP需要检测的声音音高是从C0~B8的,C0~B8 共108个音符称为标准音符(或固定音符),其中,A4又被称为国际标准音。标准音A4的频率不同,所有标准音符的频率也不同,即所有标准音符的频率是由标准音确定的。

        本调音器,默认标准音是440Hz,允许用户切换标准音频率,比如用户将标准音切换至442Hz后,若要得到某个音符的频率,程序会根据当前标准音频率通过算法求出该音符的频率。下图是我自己整理的108个音名的频率-波长对照表(当标准音为440Hz时)

        根据十二平均律:一个音的频率刚好是其低八度音频率的两倍,即若A4频率为440Hz,则A3频率为220Hz,A5频率为880Hz;若X音符为880Hz,Y音符比X音符高22个半音,则Y音符的频率 = 880 * 2^(22/12) = 3135.96 Hz,在上方的频率对照表里,我们可以查到X音符为A5,Y音符为G7。

由此,我们可以编写出根据标准音A4频率计算所有标准音符频率的算法:

  • ValueAnimator 数值动画控制器

        用来控制调音器仪表盘指针转动,并使用了自定义插值器,使动画更加丝滑。

2.节拍器-编程原理

  • 节拍器程序执行的大致流程:

         节拍器UI控件如下: 

  • ValueAnimator 数值动画控制器

        用来控制节拍器摆锤转动,并使用了自定义插值器,使动画更加丝滑。

  •  应用到的比较重点的操作:

       (1)Thread线程类

       (2)SoundPool库:用于进行7种音色音频文件的导入、播放

       (3)ViewModel:用于在不同Fragment之间进行信息传递

       (4)手机手电筒、振动控制:定义FlashUtils类来管理camera状态,进行手电筒的开关操作;新建Vibrator对象,设置振动频率,控制手机振动

       (5)UI方面采用了1个Activity装载3个fragment,3个fragment可以来回切换显示

代码框架

1.代码文件的分类

代码文件夹主要有4个,manifest(配置文件)、java(java代码文件)、res(包含color色彩配置文件、drawable贴图文件、layout布局文件、menu菜单文件、mipmap图形文件、raw源媒体文件、values色彩尺度字符主题文件)、resources(包含一个csv文件,写有所有音符对应频率和波长的数据)

2.Manifest配置文件

配置文件里主要编写了用户权限许可声明、LOGO配置、主题配置、Activity基本信息声明

用户权限:主要是APP需要向用户申请麦克风录音和手机振动的权限

  1. <uses-permission android:name="android.permission.VIBRATE" />
  2. <uses-permission android:name="android.permission.RECORD_AUDIO" />

Activity声明:MainActivity是主界面,metro.mertoMainActivity是节拍器界面,ButtonActivity是调音器界面(开始随便命了一个名,后面懒得改了hhh),SplashActivity是启动界面

  1. <activity
  2. android:name=".MainActivity"
  3. android:exported="true" />
  4. <activity
  5. android:name=".metro.metroMainActivity"
  6. android:exported="false" />
  7. <activity
  8. android:name=".ButtonActivity"
  9. android:exported="false" />
  10. <activity
  11. android:name=".SplashActivity"
  12. android:exported="true"
  13. android:hardwareAccelerated="false"
  14. android:launchMode="standard"
  15. android:configChanges="orientation|screenSize|keyboardHidden">

3.启动界面和主界面

(1)启动界面:需要写的是Java文件SplashActivity.java,还有对应的Xml布局文件

Java文件:定义SplashActivity类,并继承AppCompatActivity(Android中的Acitivity类)。在onCreate()里写的都是用户开启这个Activity之后自动运行的代码和函数。其中setContentView是指定布局文件 。定义Handler(消息传递机制),然后定义新线程run,再定义Intent,从SplashActivity(启动界面)跳转到MainActivity(主界面)。

整个功能是启动界面会显示并停留2100毫秒,然后工作线程通过Handler通知主线程(UI线程),完成界面跳转操作。

  1. public class SplashActivity extends AppCompatActivity {
  2. public static SplashActivity instance;
  3. @Override
  4. protected void onCreate(Bundle savedInstanceState) {
  5. super.onCreate(savedInstanceState);
  6. instance=this;
  7. setContentView(R.layout.activity_splash);
  8. Handler handler = new Handler();
  9. handler.postDelayed(new Runnable() {
  10. @Override
  11. public void run() {
  12. Intent intent = new Intent(SplashActivity.this,MainActivity.class);
  13. startActivity(intent);
  14. SplashActivity.this.fileList();
  15. instance.finish();
  16. }
  17. },2100);
  18. }

Acitivity界面布局:布局在layout文件夹里新建activity_splash.xml作为该启动界面Activity对应的布局文件,可以进行自定义布局。布局文件里写了1个ImageView和3个TextView。

这个启动界面的话(包括其他的所有界面也一样)主要是采用的是垂直的相对布局(RelativeLayout),就是把每个部件与屏幕边界绑定,然后设置偏移的百分比(距离屏幕边界)。也可以是每个部件之间设置相对距离。

图片的导入:我是绘制好了之后,转成SVG(矢量图)格式,由下图的方式,在屏幕左侧drawable文件夹处右键->New新建->Vector Asset,生成单个图像的xml文件,可以直接在布局文件里使用这个图片的xml文件。 

(2)主界面:需要写的是Java文件MainActivity.java,还有对应的Xml布局文件

Java文件:定义MainActivity类,主界面主要的部件是2个按钮组件(Button),我自定义了myClick函数监听按钮的触发,当按下“调音器”按钮时,运行跳转程序,跳转到调音器界面;“节拍器”按钮同上。

  1. public void myClick(View v){
  2. switch (v.getId()){
  3. case R.id.btn1:
  4. Intent intent=new Intent(MainActivity.this,ButtonActivity.class);
  5. startActivity(intent);
  6. break;
  7. case R.id.btn2:
  8. Intent intent1=new Intent(MainActivity.this, metroMainActivity.class);
  9. startActivity(intent1);
  10. break;
  11. }
  12. }

主界面还写了onKeyDown代码,定义了exit()函数,保证当用户触发退出键后,运行exit(),会发送底部消息气泡提示用户“再点击一次退出键将退出程序”。

  1. public void exit(){
  2. if (!isExit) {
  3. isExit = true;
  4. Toast.makeText(getApplicationContext(), "再按一次退出程序", Toast.LENGTH_SHORT).show();
  5. mHandler.sendEmptyMessageDelayed(0, 2000);
  6. } else {
  7. Intent intent = new Intent(Intent.ACTION_MAIN);
  8. intent.addCategory(Intent.CATEGORY_HOME);
  9. startActivity(intent);
  10. System.exit(0);
  11. }
  12. }

主界面的布局文件如下:

4.调音器界面

调音器所包含的Java文件有:ButtonActivity.java(调音器界面),CanvasPainter.java(绘制模块),ChromaticTuning.java(十二平均律调音模块),KeyView.java(钢琴键盘显示模块),ListenerFragment.java(收音模块)Note.java(接口)NoteFrequencyCalculator.java(音符频率计算)NoteName.java(枚举类,列出C0~B8的108个音符的音名和符号,包含scientific音名表示法和sol唱名表示法)PitchComparator.java(与标准音符进行比较,找出距离最近的标准音符),PitchDifference.java(计算与标准音符之间的频率差值),Sampler.java(采样器,包含每一次采样的操作,循环调用PitchDifference函数,计算所有采样信号与标准音符的频率差值,得出声音信号中出现最频繁的音符),TunerView.java(自定义View,绘制调音器界面的部分UI),Tuning.java(接口)

  主要说一下声音的接收、数字信号处理,怎么计算出与当前声音差值最小的标准音高,和图形可视化部分的实现吧。

(1)声音的接收和处理:

        在ListenerFragment.java文件中,创建Fragment,作为载体用来进行声音接收、处理与回调。

        需要使用Java第三方库TarsosDSP,这是一个音频处理库。该库是纯 Java 实现 , 没有依赖任何外部的第三方库。

  1. import be.tarsos.dsp.AudioDispatcher;
  2. import be.tarsos.dsp.io.android.AudioDispatcherFactory;
  3. import be.tarsos.dsp.pitch.PitchDetectionHandler;
  4. import be.tarsos.dsp.pitch.PitchProcessor;
  5. import be.tarsos.dsp.pitch.PitchProcessor.PitchEstimationAlgorithm;
  • 定义PitchListener类,继承AsyncTask,在该类里写所有跟音高检测有关的代码。

        AsyncTask简介:onPreExcute() (主线程中执行)、doInBackground(Params...)(子线程中执行)、如果要更新UI,可以通过调用publishProgress(Progess...)方法来完成比如反馈当前的进度、onProgressUpdate(Progress...)(主线程中执行)、onPostExecute(Result)(主线程中执行)

上面几个方法的调用顺序:onPreExcute() --> doInBackground() --> publishProgress() --> onProgressUpdate() --> onPostExcute()

在doInBackground()方法中执行接收声音信号的具体操作,在onProgressUpdate()方法中实时

AsyncTask的参考链接:八、AsyncTask解析 - 简书

        调用TarsosDSP库中的类和方法:AudioDispatcher、PitchDetectionHandler

        在PitchListener类里新建AudioDispatcher对象,这个对象采用默认麦克风作为声音信号来源,并设置最小item计数为15(MIN_ITEMS_COUNT =15),即采样15次为一组。之后在doInBackground(Params...)里新建PitchDetectionHandler对象,调用相关方法获取音高值(详见TarsosDSP库使用手册)。

float pitch = pitchDetectionResult.getPitch();

其中, AudioDispatcher负责切断帧中的音频,它还将音频帧包装到AudioEvent对象中。此AudioEvent对象通过AudioProcessors链发送。

        使用Sampler.java里编写的calculateAverageDifference方法(注意要定义为静态),采样出的信号装入List<PitchDifference> samples(samples为由多个PitchDifference类对象组成的列表),提取采样序列中出现频率最高的音高,并采样序列进行过滤;计算过滤后序列的总偏离值(deviation),然后计算平均偏离值,最后返回。P.S 其中频率最高的音符定义为Note接口类型;提取采样序列中出现频率最高的音高是使用Sampler.java里编写的extractMostFrequentNote方法;Sampler.java里的filterByNote方法对采样序列samples进行过滤,遍历该组采样,判断距离每一个sample音高最近的标准音高是否为本组采样出现最多的音高(代码中用sample.closest表示距离sample音高最近的标准音符,其中,sample为从samples遍历出的pitchDifference对象),最后返回过滤后的采样组filteredSamples。

extractMostFrequentNote方法:定义Hash表noteFrequencies(音符频率),noteFrequenceis.containsKey(closest)判断Hash表中是否存在closest这个Key对应的映射关系(键值对映射关系),若有,取出与此声音信号音高距离最近的标准音高,获取最近标准音高的记录值(count),count+1后保存到Hash表中;若无,则将其添加到表中,count值记为1。(即对每一个sample对应的最近标准音高进行计数)

  1. static Note extractMostFrequentNote(List<PitchDifference> samples) {
  2. Map<Note, Integer> noteFrequencies = new HashMap<>();
  3. for (PitchDifference pitchDifference : samples) {
  4. Note closest = pitchDifference.closest;
  5. if (noteFrequencies.containsKey(closest)) {
  6. Integer count = noteFrequencies.get(closest);//获取Key对应的Value
  7. noteFrequencies.put(closest, count + 1);//将键/值添加到Hash表中
  8. } else {
  9. noteFrequencies.put(closest, 1);
  10. }
  11. }
  12. Note mostFrequentNote = null;
  13. int mostOccurrences = 0;
  14. //noteFrequencies.keySet()返回表中Key的集合
  15. for (Note note : noteFrequencies.keySet()) {
  16. Integer occurrences = noteFrequencies.get(note);
  17. if (occurrences > mostOccurrences) {
  18. mostFrequentNote = note;
  19. mostOccurrences = occurrences;
  20. }
  21. }
  22. return mostFrequentNote;
  23. }

注意下面定义的calculateAverageDifference方法是返回值为PitchDifference类的方法。

static PitchDifference calculateAverageDifference(List<PitchDifference> samples) {

        Sampler.calculateAverageDifference()返回对采样序列pitchDifferences计算出的出现最多的音符(mostFrequentNote)和平均音准偏差(averageDeviation)所新建的PitchDifference对象。

return new PitchDifference(mostFrequentNote, averageDeviation);

        ListenerFragment.java中,每组采样得到上述返回的新建PitchDifference对象average,并对其调用publishProgress(average),publishProgress()用来在PitchListener类(继承了AsyncTask)中更新UI。

  1. if (pitchDifferences.size() >= MIN_ITEMS_COUNT) {
  2. PitchDifference average =
  3. Sampler.calculateAverageDifference(pitchDifferences);
  4. publishProgress(average);
  5. pitchDifferences.clear();
  6. }

        创建pitchProcessor对象,初始化设置采样频率为44100,缓冲区大小1024*4,重叠区768*4,音高估计算法设置为PitchEstimationAlgorithm.FFT_YIN(一种快速傅里叶变换算法)。接着初始化audioDispatcher对象,将新建的pitchProcessor添加audioDispatcher对象中,运行audioDispatcher

        接着在PitchListener类(继承了AsyncTask)中的onProgressUpdate()方法里调用接口taskCallbacks的onProgressUpdate()方法,即在主线程中运行

P.S PitchListener类(自己定义的)定义如下:

  1. private static class PitchListener extends AsyncTask<Void, PitchDifference, Void> {
  2. private AudioDispatcher audioDispatcher;
  3. @Override
  4. protected Void doInBackground(Void... params) {
  5. PitchDetectionHandler pitchDetectionHandler = (pitchDetectionResult, audioEvent) -> {
  6. if (isCancelled()) {
  7. stopAudioDispatcher();
  8. return;
  9. }
  10. if (!IS_RECORDING) {
  11. IS_RECORDING = true;
  12. publishProgress();
  13. }
  14. float pitch = pitchDetectionResult.getPitch();
  15. if (pitch != -1) {
  16. PitchDifference pitchDifference = PitchComparator.retrieveNote(pitch);
  17. pitchDifferences.add(pitchDifference);
  18. if (pitchDifferences.size() >= MIN_ITEMS_COUNT) {
  19. PitchDifference average =
  20. Sampler.calculateAverageDifference(pitchDifferences);
  21. publishProgress(average);
  22. pitchDifferences.clear();
  23. }
  24. }
  25. };
  26. PitchProcessor pitchProcessor = new PitchProcessor(PitchEstimationAlgorithm.FFT_YIN,
  27. SAMPLE_RATE,
  28. BUFFER_SIZE, pitchDetectionHandler);
  29. audioDispatcher = AudioDispatcherFactory.fromDefaultMicrophone(SAMPLE_RATE,
  30. BUFFER_SIZE, OVERLAP);
  31. audioDispatcher.addAudioProcessor(pitchProcessor);
  32. audioDispatcher.run();
  33. return null;
  34. }
  35. @Override
  36. protected void onProgressUpdate(PitchDifference... pitchDifference) {
  37. if (taskCallbacks != null) {
  38. if (pitchDifference.length > 0) {
  39. taskCallbacks.onProgressUpdate(pitchDifference[0]);
  40. } else {
  41. taskCallbacks.onProgressUpdate(null);
  42. }
  43. }
  44. }
  45. @Override
  46. protected void onCancelled(Void result) {
  47. stopAudioDispatcher();
  48. }
  49. private void stopAudioDispatcher() {
  50. if (audioDispatcher != null && !audioDispatcher.isStopped()) {
  51. audioDispatcher.stop();
  52. IS_RECORDING = false;
  53. }
  54. }
  55. }
  56. }

然后在ListenerFragment中的onCreate方法里创建PitchListener对象,执行对象所带有的内容。

  • 定义接口TaskCallbacks,定义方法onProgressUpdate,调用。TaskCallbacks接口是在ButtonActivity类中实现的。

下面给出这个接口Java类的关系示意图:

(2)如何计算出与当前声音频率差值最小的标准音高:

        在上一步中我们可以得到当前麦克风所接收声音的频率,我们需要得到它离哪一个标准音符最近,相差多少频率。

        在PitchDifference.java文件中,定义了一个PitchDifference抽象类,并implements Parcelable接口。Parcelable是Android提供的序列化接口,实现序列化和反序列化的操作。

为什么要用Parcelable呢?Parcelable作用是利用Parcel out 将数据存储到内存中,然后通过Parcel in 从内存中获取数据。进行Android开发的时候,无法将对象的引用传给Activities,我们需要将这些对象放到一个Intent或者Bundle里面,然后再传递。简单来说就是将对象转换为可以传输的二进制流(二进制序列)的过程,这样我们就可以通过序列化,转化为可以在网络传输或者保存到本地的流(序列),从而进行传输数据 ,那反序列化就是从二进制流(序列)转化为对象的过程。Parcel提供了一套机制,可以将序列化之后的数据写入到一个共享内存中,其他进程通过Parcel可以从这块共享内存中读出字节流,并反序列化成对象。

        PitchDifference类的成员变量是Note型的closest,即距离最近的标准音符(Note是在Note.java文件中自定义的接口)和double型的deviation,即距离标准音符的偏差值。

        createFromParcel(Parcel in) 从Parcel容器中读取传递数据值,封装成Parcelable对象返回逻辑层。newArray(int size) 创建一个类型为T,长度为size的数组,仅一句话(return new T[size])即可(暂时感觉没用)。

        private PitchDifference(Parcel in){...},是在创建构造方法,实例化对象。

        writeToParcel 方法,将对象写入序列化,即打包需要传递的数据到Parcel容器保存。

  1. class PitchDifference implements Parcelable {
  2. public static final Creator<PitchDifference> CREATOR = new Creator<PitchDifference>() {
  3. public PitchDifference createFromParcel(Parcel in) {
  4. return new PitchDifference(in);
  5. }
  6. public PitchDifference[] newArray(int size) {
  7. return new PitchDifference[size];
  8. }
  9. };
  10. final Note closest;
  11. final double deviation;
  12. PitchDifference(Note closest, double deviation) {
  13. this.closest = closest;
  14. this.deviation = deviation;
  15. }
  16. private PitchDifference(Parcel in) {
  17. Tuning tuning = ButtonActivity.getCurrentTuning();
  18. closest = tuning.findNote(in.readString());
  19. deviation = in.readDouble();
  20. }
  21. @Override
  22. public int describeContents() {
  23. return 0;
  24. }
  25. @Override
  26. public void writeToParcel(Parcel dest, int flags) {
  27. dest.writeString(closest.getName().name());
  28. dest.writeDouble(deviation);
  29. }
  30. }

不了解Parcelable的话可以参考一下下面的博文: 

 第二章——Parcelable接口的使用(跨进程,Intent传输)_dianqu7487的博客-CSDN博客

        NoteFrequencyCalculator.java:用来计算改变标准音之后的所有标准音符C0~B8的固定频率(比如从440Hz切换到442Hz后)

        NoteName.java中定义了枚举类NoteName,CDEFGAB 7个音名对应的两种表示法:科学表示法(scientific,用音名表示音符)+唱名表示法(sol,用唱名表示音符)

  1. C("C", "Do"),
  2. D("D", "Re"),
  3. E("E", "Mi"),
  4. F("F", "Fa"),
  5. G("G", "Sol"),
  6. A("A", "La"),
  7. B("B", "Si");
  8. private final String scientific;
  9. private final String sol;

        Tuning.java中定义了接口Tuning,包含返回Note[]型的getNotes()方法,还有通过音名查找的方法findNote()。

  1. public interface Tuning {
  2. Note[] getNotes();
  3. Note findNote(String name);
  4. }

        Note.java中定义了接口Note,包含返回音符名的getName()方法 ,返回八度记号(octave)的getOctave()方法,以及返回升降号标记(Sign)的getSign()方法。

  1. public interface Note {
  2. NoteName getName();
  3. int getOctave();
  4. String getSign();
  5. }

        在ChromaticTuning.java文件里,定义ChromaticTuning类实现接口Tuning,@Override重写接口中的方法getNotes()findNote()。Pitch.values()用来将枚举类转变为一个枚举类型的数组;Pitch.valueOf(name)输入string型的音名name然后输出对应的枚举常量对象。

  1. @Override
  2. public Note[] getNotes() {
  3. return Pitch.values();
  4. }
  5. @Override
  6. public Note findNote(String name) {
  7. return Pitch.valueOf(name);
  8. }

其中又定义一个枚举类Pitch实现接口Note。getNotes()返回枚举类型的对象数组,该方法可以很方便地遍历所有的枚举值。下面代码为枚举类中列出的-1八度的12个音符的详细信息(包含音名、octave和升降号),第-1~第9八度共120个音符都需要枚举出来。注意:音名name需要定义为NoteName类(NoteName是自定义的枚举类,用于选择性用两种命名法表示音符的音名)

  1. C_MINUS_1(C, -1),
  2. C_MINUS_1_SHARP(C, -1, "#"),
  3. D_MINUS_1(D, -1),
  4. D_MINUS_1_SHARP(D, -1, "#"),
  5. E_MINUS_1(E, -1),
  6. F_MINUS_1(F, -1),
  7. F_MINUS_1_SHARP(F, -1, "#"),
  8. G_MINUS_1(G, -1),
  9. G_MINUS_1_SHARP(G, -1, "#"),
  10. A_MINUS_1(A, -1),
  11. A_MINUS_1_SHARP(A, -1, "#"),
  12. B_MINUS_1(B, -1),

PitchComparator.java中,定义了PitchComparator类

retrieveNote():从ButtonActivity获取当前选择的调音模式,新建十二平均律调音(Chromatic Tuning)对应的Tuning接口对象,并从ButtonActivity获取当前选择的参考标准音(reference Pitch)。创建NoteFrequencyCalculator对象,用于计算当前参考标准音下所有标准音符的具体频率。最后求出距离采样音符组note最近的标准音符(closest),以及最小的音分偏差(minCentDifference)。最后,retrieveNote()方法返回用这两者新建的PitchDifference对象。

  1. class PitchComparator {
  2. static PitchDifference retrieveNote(float pitch) {
  3. ...
  4. double minCentDifference = Float.POSITIVE_INFINITY;
  5. Note closest = notes[0];
  6. for (Note note : notes) {
  7. double frequency = noteFrequencyCalculator.getFrequency(note);
  8. double centDifference = 1200d * log2(pitch / frequency);
  9. if (Math.abs(centDifference) < Math.abs(minCentDifference)) {
  10. minCentDifference = centDifference;
  11. closest = note;
  12. }
  13. }
  14. return new PitchDifference(closest, minCentDifference);
  15. ...
  16. private static double log2(double number) {
  17. return Math.log(number) / Math.log(2);
  18. }
  19. }

为什么要定义Tuning接口,然后通过ChromaticTuning类来实现接口所预设的功能?

实际上是本项目参考的GitHub项目,原本是考虑设定不同乐器有其特定的调音模式,可以让用户自由选择不同的调音模式。用户可以选择不同的调音模式,选定某个调音模式,程序进行监听,新建目标模式对应的调音类(比如十二平均律调音模型就对应ChromaticTuning类)。每一种调音类都implements Tuning接口,比如:public class ChromaticTuning implements Tuning(){...}

这样就可以分调音模式,实现不同的调音功能(但本APP没有设计这一切换调音模式的功能,主要是感觉能把基础的十二平均律调音模式写好都不错了hhh)

(3)调音器界面的UI设计:

调音器界面的布局文件如下:

        在调音器界面(本项目代码将其随便命名为ButtonActivity)的Acitivity里,询问用户获取录音权限,创建ListenerFragment对象,开始录音,并进行数字信号处理,得到声音频率信息;之后通过PitchDifference、PitchComparator类编写的算法,根据设定的标准音频率(440Hz或442Hz),获取距离此时声音音高最近的标准音符最小音分偏差,然后将这俩信息还有当前声音频率对应音分实时更新到UI上,不断循环此过程。

        ButtonActivity.java文件主要来编写UI更新与监听代码。在 ButtonActivity类里实现TaskCallBacks接口,用于实时更新UI。

  • 调音器界面放置了2个SwitchButton,用户可以选定标准音频率音名表示方法

  • 对于UI的绘制:

        定义了两个自定义View:TunerView.java(绘制仪表盘界面)KeyView.java(绘制底部的小钢琴键盘)

        还有CanvasPainter.java 用于初始化画笔参数

        主要使用了canvas库来实时绘制图像,并利用矩阵旋转,来实现仪表盘指针的旋转。

未完待续~

5.节拍器界面

(1)音频有节奏播放

        HomeFragment.java 主循环主要都是在监听普通按钮(Button)和切换按钮(Switch Button)。

        定义Tick类并继承Thread类(线程类),在Thread类run()方法中编写需要循环执行的程序(也就是周期性不断播放节拍器声音)。

  1. class Tick extends Thread {
  2. protected int tempo, subdiv, nTicks,beats,soundNum;
  3. protected long startTime, nextTime;
  4. boolean keepGoing;
  5. long detime;
  6. boolean pendl;
  7. public Tick(int tempo, int beats,int subdiv,int soundNum) {
  8. this.tempo = tempo;
  9. this.beats = beats;
  10. this.subdiv = subdiv;
  11. this.soundNum = soundNum;
  12. keepGoing = true;
  13. }
  14. @Override public void run() {
  15. nTicks = 0;
  16. startTime = System.currentTimeMillis();
  17. nextTime = startTime;
  18. pendl=true;
  19. while (keepGoing) {
  20. ......
  21. }
  22. ......
  23. }

(2)参数实时更新

        问题:如何实现用户点击按钮后,能够瞬间感受到参数改变带来的效果?

        编写实时改变参数的方法。在按钮被点击之后,监听程序调用该方法,来修改tick对象中的参数,这样线程对象tick就会在循环中,使用新的参数来进行音频播放。

(3)不同Fragment之间信息传输

        问题:如何实现在第三个Fragment(设置界面)里点击多选项按钮修改音色后,第一个Fragment能够即时接收到该信号,对线程对象tick中的相应参数进行修改?

        使用SoundPool进行音频播放,是通过切换音频ID来实现播放不同音频文件的。因此,可以定义一个新的SoundNum类,用来储存SoundNumber也就是音频ID(int型)。定义ViewModel类,创建一个ViewModel对象myViewModel,将SoundNum对象绑定在myViewModel中,来监听SoundNum对象数据是否变化:若在第三个Fragment里修改了SoundNum对象的数据,则myViewModel可以监听到,然后可以在第一个Fragment里编写监听后的操作。

P.S. 注意myViewModel的实例化要在@override onActivityCreated(Bundle savedInstanceState){...}里进行。

  1. public class HomeFragment extends BaseFragment{
  2. private MyViewModel myViewModel;
  3. @Override
  4. public void onActivityCreated(Bundle savedInstanceState) {
  5. super.onActivityCreated(savedInstanceState);
  6. myViewModel = new ViewModelProvider(
  7. requireActivity(),
  8. new ViewModelProvider.NewInstanceFactory()).get(MyViewModel.class);
  9. myViewModel.mUserLiveData.observe(getActivity(), new Observer<SoundNum>() {
  10. @Override
  11. public void onChanged(SoundNum soundNum) {
  12. tick.soundNum = soundNum.soundNum;
  13. }
  14. });
  15. }
  16. }

        节拍器UI部分:

metroMainActivity(节拍器主界面)布局文件如下:

HomeFragment(节拍器显示界面)布局文件 如下:

ThirdFragment(设置界面)布局文件 如下: 

项目总结

遇到的问题

在fragment里需要用this、载体之类的变量类型,可以尝试用getActivity()

未完待续~

总结

未完待续~

本项目开源地址:沐尘调音器-节拍器: 本APP是音乐类Android应用程序,主要提供调音器和节拍器功能。

本项目所参考的开源项目,以及使用的第三方库TarsosDSP的地址如下,大家可以了解一下:

GitHub - gstraube/cythara: A musical instrument tuner for Android

GitHub - JorenSix/TarsosDSP: A Real-Time Audio Processing Framework in Java

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/从前慢现在也慢/article/detail/797772
推荐阅读
相关标签
  

闽ICP备14008679号