ภาพรวม
บทความนี้สาธิตวิธีสร้างและเล่นเสียงโทน (Tone) ด้วยความถี่เฉพาะ (Arbitrary Tone) ใน Android โดยใช้ AudioTrack ในโหมด Static ซึ่งเหมาะสำหรับการสร้างเสียง Beep, DTMF, Musical Notes, หรือ Tones สำหรับการทดสอบอุปกรณ์เสียง
ขั้นตอนที่ 1: สร้างโปรเจกต์ใหม่
สร้างโปรเจกต์ Android ใหม่ใน Android Studio
ขั้นตอนที่ 2: ออกแบบ Layout (res/layout/activity_main.xml)
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="https://schemas.android.com/apk/res/android"
xmlns:tools="https://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:padding="16dp"
tools:context=".MainActivity">
<ImageView
android:layout_width="200dp"
android:layout_height="300dp"
android:src="@drawable/ic_music_note_black_24dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="เพิ่มระดับเสียง Emulator ฟังเสียง Tone!"
android:textSize="16sp"
android:textStyle="bold|italic" />
</LinearLayout>
ขั้นตอนที่ 3: เขียนโค้ด MainActivity (src/MainActivity.java) - เวอร์ชันปรับปรุง
import androidx.appcompat.app.AppCompatActivity;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioTrack;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.widget.Button;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.SeekBar;
import android.widget.TextView;
import android.widget.Toast;
public class MainActivity extends AppCompatActivity {
// พารามิเตอร์เสียง
private final int duration = 2; // วินาที
private final int sampleRate = 44100; // Hz (CD Quality)
private int frequency = 440; // Hz (A4 Note)
private final int numSamples;
private final double[] sample;
private final byte[] generatedSnd;
private AudioTrack audioTrack;
private Handler handler = new Handler(Looper.getMainLooper());
public MainActivity() {
numSamples = duration * sampleRate;
sample = new double[numSamples];
generatedSnd = new byte[2 * numSamples];
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// สร้าง UI แบบ Dynamic สำหรับทดสอบความถี่ต่างๆ
LinearLayout layout = new LinearLayout(this);
layout.setOrientation(LinearLayout.VERTICAL);
layout.setPadding(32, 32, 32, 32);
layout.setGravity(android.view.Gravity.CENTER_HORIZONTAL);
TextView title = new TextView(this);
title.setText("🎵 Tone Generator");
title.setTextSize(24);
title.setTypeface(null, android.graphics.Typeface.BOLD);
layout.addView(title);
// Frequency Control
TextView freqLabel = new TextView(this);
freqLabel.setText("ความถี่: " + frequency + " Hz");
freqLabel.setTextSize(18);
layout.addView(freqLabel);
SeekBar freqSeekBar = new SeekBar(this);
freqSeekBar.setMax(2000); // 200 - 2200 Hz
freqSeekBar.setProgress(frequency - 200);
freqSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
frequency = progress + 200;
freqLabel.setText("ความถี่: " + frequency + " Hz");
}
@Override public void onStartTrackingTouch(SeekBar seekBar) {}
@Override public void onStopTrackingTouch(SeekBar seekBar) {}
});
layout.addView(freqSeekBar);
// Duration Control
TextView durLabel = new TextView(this);
durLabel.setText("ระยะเวลา: " + duration + " วินาที");
durLabel.setTextSize(18);
layout.addView(durLabel);
SeekBar durSeekBar = new SeekBar(this);
durSeekBar.setMax(5); // 1-5 วินาที
durSeekBar.setProgress(duration - 1);
durSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
durLabel.setText("ระยะเวลา: " + (progress + 1) + " วินาที");
}
@Override public void onStartTrackingTouch(SeekBar seekBar) {}
@Override public void onStopTrackingTouch(SeekBar seekBar) {}
});
layout.addView(durSeekBar);
Button playBtn = new Button(this);
playBtn.setText("เล่นเสียง Tone");
playBtn.setOnClickListener(v -> playTone(frequency, duration));
layout.addView(playBtn);
Button stopBtn = new Button(this);
stopBtn.setText("หยุด");
stopBtn.setOnClickListener(v -> stopTone());
layout.addView(stopBtn);
setContentView(layout);
}
private void playTone(int freqHz, int durSec) {
// Generate Tone ใน Background Thread
new Thread(() -> {
generateTone(freqHz, durSec);
handler.post(() -> {
playSound();
Toast.makeText(this, "เล่นเสียง " + freqHz + " Hz", Toast.LENGTH_SHORT).show();
});
}).start();
}
private void generateTone(int freqHz, int durSec) {
int samples = durSec * sampleRate;
// ปรับขนาด Array หาก duration เปลี่ยน
if (samples != numSamples) {
// สำหรับความเรียบง่าย ใช้ค่าคงที่ ต้องสร้าง Array ใหม่ถ้าอยากเปลี่ยน Duration
}
// สร้าง Sine Wave
for (int i = 0; i < numSamples; ++i) {
sample[i] = Math.sin(2 * Math.PI * i / (sampleRate / (double) freqHz));
}
// แปลงเป็น 16-bit PCM (Little Endian)
int idx = 0;
for (double dVal : sample) {
short val = (short) (dVal * 32767); // Scale to 16-bit range
generatedSnd[idx++] = (byte) (val & 0x00FF);
generatedSnd[idx++] = (byte) ((val & 0xFF00) >>> 8);
}
}
private void playSound() {
stopTone(); // หยุดเสียงเก่าก่อน
audioTrack = new AudioTrack.Builder()
.setAudioAttributes(new android.media.AudioAttributes.Builder()
.setUsage(android.media.AudioAttributes.USAGE_MEDIA)
.setContentType(android.media.AudioAttributes.CONTENT_TYPE_MUSIC)
.build())
.setAudioFormat(new AudioFormat.Builder()
.setEncoding(AudioFormat.ENCODING_PCM_16BIT)
.setSampleRate(sampleRate)
.setChannelMask(AudioFormat.CHANNEL_OUT_MONO)
.build())
.setBufferSizeInBytes(generatedSnd.length)
.setTransferMode(AudioTrack.MODE_STATIC)
.build();
audioTrack.write(generatedSnd, 0, generatedSnd.length);
audioTrack.play();
}
private void stopTone() {
if (audioTrack != null) {
audioTrack.stop();
audioTrack.release();
audioTrack = null;
}
}
@Override
protected void onDestroy() {
super.onDestroy();
stopTone();
}
}
ขั้นตอนที่ 4: AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="https://schemas.android.com/apk/res/android"
package="app.com.sample">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
คู่มือความถี่โน้ตเพลง (Musical Notes Frequency)
| โน้ต | ความถี่ (Hz) | โน้ต | ความถี่ (Hz) |
|---|---|---|---|
| C4 (Middle C) | 261.63 | C5 | 523.25 |
| D4 | 293.66 | D5 | 587.33 |
| E4 | 329.63 | E5 | 659.25 |
| F4 | 349.23 | F5 | 698.46 |
| G4 | 392.00 | G5 | 783.99 |
| A4 (Standard) | 440.00 | A5 | 880.00 |
| B4 | 493.88 | B5 | 987.77 |
การประยุกต์ใช้งาน
- DTMF Dialer: ผสม 2 ความถี่ (Row + Column) สำหรับหมายเลข 0-9, *, #
- Hearing Test: ทดสอบช่วงความถี่ที่ได้ยินได้ (20Hz - 20kHz)
- Instrument Tuner: เปรียบเทียบ Pitch กับ Reference Frequency
- Modem/FSK: Frequency Shift Keying สำหรับ Data Transmission
- Accessibility: Audio Feedback สำหรับผู้มีปัญหาการมองเห็น
ข้อควรระวัง
- Sample Rate: 44100 Hz เป็นมาตรฐาน รองรับทุกอุปกรณ์ (8000 Hz คุณภาพต่ำ)
- Buffer Size: MODE_STATIC ต้องมี Buffer ใหญ่พอสำหรับข้อมูลทั้งหมด
- Thread Safety: Generate Tone ใน Background Thread, Play บน Main Thread
- Volume: ใช้ AudioAttributes แทน setStreamVolume (Deprecated)
- Memory: ทोनยาวๆ ใช้ RAM มาก (44100 * 2 bytes * duration)
ตัวเลือกอื่น: ToneGenerator (API 1+)
// สำหรับ DTMF/Standard Tones เท่านั้น ไม่รองรับ Arbitrary Frequency ToneGenerator toneGenerator = new ToneGenerator(AudioManager.STREAM_MUSIC, 100); toneGenerator.startTone(ToneGenerator.TONE_DTMF_1, 500); // เล่น 500ms toneGenerator.release();