Computer >> บทช่วยสอนคอมพิวเตอร์ >  >> การเขียนโปรแกรม >> Android

วิธีเล่นเสียงโทน (Tone) เฉพาะความถี่ใน Android

ภาพรวม

บทความนี้สาธิตวิธีสร้างและเล่นเสียงโทน (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.63C5523.25
D4293.66D5587.33
E4329.63E5659.25
F4349.23F5698.46
G4392.00G5783.99
A4 (Standard)440.00A5880.00
B4493.88B5987.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();