【安卓开发】【Android】做一个简单的钢琴模拟器
说明
仅作为学习用途。
一、效果展示
从豪华别墅进入钢琴房,点击钢琴,呼出键盘(约1个八度),点击相应的键位,即可弹奏。效果如下:
piano
图片效果:
二、活动代码
// 颐居·沙发房
package com.lycbuaacs.my_hg;import static android.view.View.INVISIBLE;
import static android.view.View.VISIBLE;import android.annotation.SuppressLint;
import android.content.Intent;
import android.graphics.Typeface;
import android.media.AudioAttributes;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioTrack;
import android.os.Bundle;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;import androidx.activity.EdgeToEdge;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;public class MainActivity23 extends AppCompatActivity {public static class ToneGenerator {private static final int SAMPLE_RATE = 44100; // 采样率public void playTone(double frequencyHz, int durationMs) {// 1. 计算所需样本数int numSamples = (int)(durationMs * SAMPLE_RATE / 1000.0);short[] buffer = new short[numSamples];double angularFrequency = 2 * Math.PI * frequencyHz / SAMPLE_RATE;// 2. 生成正弦波样本for (int i = 0; i < numSamples; i++) {double sample = Math.sin(i * angularFrequency);buffer[i] = (short)(sample * Short.MAX_VALUE); // 16位PCM}// 3. 配置AudioTrackAudioTrack audioTrack = new AudioTrack(new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_MEDIA).setContentType(AudioAttributes.CONTENT_TYPE_MUSIC).build(),new AudioFormat.Builder().setSampleRate(SAMPLE_RATE).setEncoding(AudioFormat.ENCODING_PCM_16BIT).setChannelMask(AudioFormat.CHANNEL_OUT_MONO).build(),buffer.length * 2, // 缓冲区大小(字节)AudioTrack.MODE_STATIC,AudioManager.AUDIO_SESSION_ID_GENERATE);// 4. 播放音频audioTrack.write(buffer, 0, buffer.length);audioTrack.play();// 5. 释放资源(实际使用需异步处理)new Thread(() -> {try {Thread.sleep(durationMs);} catch (InterruptedException e) {e.printStackTrace();}audioTrack.stop();audioTrack.release();}).start();}}int play = 0;@SuppressLint("ClickableViewAccessibility")@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);EdgeToEdge.enable(this);setContentView(R.layout.activity_main23);ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);return insets;});ToneGenerator ge = new ToneGenerator();ImageView image = findViewById(R.id.ins_1);// 羽毛图片(拖拽效果)
// ImageView feather = findViewById(R.id.fea);ImageView clear = findViewById(R.id.clear);SingleTouchView pic1 = findViewById(R.id.pic1);ImageView pic2 = findViewById(R.id.pic2);TextView text = findViewById(R.id.pal_1);Typeface type = Typeface.createFromAsset(getAssets(), "kaishu.TTF");text.setTypeface(type);EditText ed_ = findViewById(R.id.reply);
// image_2.setImageResource(R.drawable.b_s_5);
// image_2.setVisibility(VISIBLE);clear.setOnClickListener(v -> {pic1.setVisibility(INVISIBLE);pic2.setVisibility(INVISIBLE);text.setVisibility(INVISIBLE);});image.setOnTouchListener((v1, event) -> {String x = new String(String.valueOf(event.getX()));String y = new String(String.valueOf(event.getY()));Toast.makeText(getApplicationContext(), x, Toast.LENGTH_SHORT).show();Toast.makeText(getApplicationContext(), y, Toast.LENGTH_SHORT).show();// 1、测试:(471,594) +- 25if ((event.getX() > 446) && (event.getX() < 496) && (event.getY() > 569) && (event.getY() < 619)) {text.setText(R.string.line_132);text.setVisibility(VISIBLE);pic1.setImageDrawable(R.drawable.mi_1);pic1.setVisibility(VISIBLE);}
// 2、钢琴:(809,764) +- 25if ((event.getX() > 784) && (event.getX() < 834) && (event.getY() > 739) && (event.getY() < 789)) {text.setText(R.string.line_167);text.setVisibility(VISIBLE);pic2.setImageResource(R.drawable.piano);pic2.setVisibility(VISIBLE);play = 2;}return false;});pic2.setOnTouchListener((v1, event) -> {String x = new String(String.valueOf(event.getX()));String y = new String(String.valueOf(event.getY()));Toast.makeText(getApplicationContext(), x, Toast.LENGTH_SHORT).show();Toast.makeText(getApplicationContext(), y, Toast.LENGTH_SHORT).show();// 1、do:(838,293) +- x +- 15 y +- 30if ((event.getX() > 823) && (event.getX() < 853) && (event.getY() > 278) && (event.getY() < 338)) {ge.playTone(261.63, 800); // do}
// 2、#do:(879,112) +- x +- 15 y +- 30if ((event.getX() > 864) && (event.getX() < 894) && (event.getY() > 82) && (event.getY() < 142)) {ge.playTone(277.20, 800); // #do}
// 3、re:(917,293) +- x +- 15 y +- 30if ((event.getX() > 902) && (event.getX() < 932) && (event.getY() > 263) && (event.getY() < 323)) {ge.playTone(293.69, 800); // re}
// 4、#re:(963,112) +- x +- 15 y +- 30if ((event.getX() > 948) && (event.getX() < 978) && (event.getY() > 82) && (event.getY() < 142)) {ge.playTone(311.16, 800); // #re}
// 5、mi:(984,293) +- x +- 15 y +- 30if ((event.getX() > 969) && (event.getX() < 999) && (event.getY() > 263) && (event.getY() < 323)) {ge.playTone(329.68, 800); // re}
// 6、fa:(1056,293) +- x +- 15 y +- 30if ((event.getX() > 1041) && (event.getX() < 1071) && (event.getY() > 263) && (event.getY() < 323)) {ge.playTone(349.29, 800); // re}
// 7、#fa:(1094,112) +- x +- 15 y +- 30if ((event.getX() > 1079) && (event.getX() < 1109) && (event.getY() > 82) && (event.getY() < 142)) {ge.playTone(370.08, 800); // #re}
// 8、sol:(1124,293) +- x +- 15 y +- 30if ((event.getX() > 1109) && (event.getX() < 1139) && (event.getY() > 263) && (event.getY() < 323)) {ge.playTone(392.10, 800); // re}
// 9、#sol:(1161,112) +- x +- 15 y +- 30if ((event.getX() > 1146) && (event.getX() < 1176) && (event.getY() > 82) && (event.getY() < 142)) {ge.playTone(415.43, 800); // #re}
// 10、la:(1200,293) +- x +- 15 y +- 30if ((event.getX() > 1185) && (event.getX() < 1215) && (event.getY() > 263) && (event.getY() < 323)) {ge.playTone(440.145, 800); // re}
// 11、bsi:(1247,112) +- x +- 15 y +- 30if ((event.getX() > 1232) && (event.getX() < 1262) && (event.getY() > 82) && (event.getY() < 142)) {ge.playTone(466.33, 800); // #re}
// 12、si:(1266,293) +- x +- 15 y +- 30if ((event.getX() > 1251) && (event.getX() < 1281) && (event.getY() > 263) && (event.getY() < 323)) {ge.playTone(494.08, 800); // re}
// 13、do:(1343,293) +- x +- 15 y +- 30if ((event.getX() > 1328) && (event.getX() < 1358) && (event.getY() > 263) && (event.getY() < 323)) {ge.playTone(523.26, 800); // re}return false;});}
}
二、音阶原理(高中数学内容)
标准的C大调,中央C(do-4)的频率为261.63赫兹(更标准的参考为la-4,频率为440赫兹)。从中央C上行一个八度,到达do-5,频率翻倍,即为523.26赫兹。从do-4上行至do-5,中间共12个半音(do, #do, re, #re, mi, fa, #fa, sol, #sol, la, bsi, si),呈等比数列递增,公比为2开12次根号。如何求得这个公比值呢?我们利用Python,类似牛顿·拉夫森法,简便地得到近似值。代码如下:
import mathstart = 1
while math.pow(start, 12) <= 1.999999:start += 0.0001print(start)
print(math.pow(1.0594999999999934, 12))
print(math.pow(1.0595, 12))
好了,就用1.0595作为2开12次根号的近似值。然后,就可以依次计算出每一个半音的音高(赫兹值),例如#do为277.20,re为293.69,以此类推。就有了上述活动代码当中,每个音符对应的频率值。据此,调用ToneGenerator的playTone()函数,即可播放对应频率的音高。