package com.aethex.os; import android.media.AudioAttributes; import android.media.AudioFormat; import android.media.AudioTrack; import java.util.EnumMap; import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /** * Singleton sound manager for AeThexOS Android. * Generates tones programmatically using AudioTrack - no sound asset files needed. */ public class SoundManager { private static final int SAMPLE_RATE = 44100; private static final float VOLUME = 0.3f; public enum Sound { OPEN(523, 0.1, WaveType.SINE), CLOSE(392, 0.1, WaveType.SINE), CLICK(800, 0.03, WaveType.SQUARE), NOTIFICATION(880, 0.15, WaveType.SINE), SWITCH(440, 0.2, WaveType.SAWTOOTH), BOOT_BEEP(660, 0.05, WaveType.SINE); final int frequency; final double duration; final WaveType waveType; Sound(int frequency, double duration, WaveType waveType) { this.frequency = frequency; this.duration = duration; this.waveType = waveType; } } private enum WaveType { SINE, SQUARE, SAWTOOTH } private static volatile SoundManager instance; private final Map bufferCache = new EnumMap<>(Sound.class); private final ExecutorService executor = Executors.newSingleThreadExecutor(); private volatile boolean enabled = true; private SoundManager() { } public static SoundManager getInstance() { if (instance == null) { synchronized (SoundManager.class) { if (instance == null) { instance = new SoundManager(); } } } return instance; } public void setEnabled(boolean enabled) { this.enabled = enabled; } public boolean isEnabled() { return enabled; } /** * Play a sound on a background thread. Non-blocking. */ public void play(Sound sound) { if (!enabled) return; executor.execute(() -> { try { byte[] pcm = getOrGenerateBuffer(sound); int bufferSize = pcm.length; AudioAttributes attributes = new AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION) .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) .build(); AudioFormat format = new AudioFormat.Builder() .setSampleRate(SAMPLE_RATE) .setEncoding(AudioFormat.ENCODING_PCM_16BIT) .setChannelMask(AudioFormat.CHANNEL_OUT_MONO) .build(); AudioTrack track = new AudioTrack.Builder() .setAudioAttributes(attributes) .setAudioFormat(format) .setBufferSizeInBytes(bufferSize) .setTransferMode(AudioTrack.MODE_STATIC) .build(); track.write(pcm, 0, pcm.length); track.setVolume(VOLUME); track.play(); // Wait for playback to complete, then release long durationMs = (long) (sound.duration * 1000) + 50; Thread.sleep(durationMs); track.stop(); track.release(); } catch (Exception e) { // Silently ignore sound playback failures } }); } private synchronized byte[] getOrGenerateBuffer(Sound sound) { byte[] cached = bufferCache.get(sound); if (cached != null) return cached; byte[] buffer = generatePcmBuffer(sound.frequency, sound.duration, sound.waveType); bufferCache.put(sound, buffer); return buffer; } private byte[] generatePcmBuffer(int frequency, double duration, WaveType waveType) { int numSamples = (int) (SAMPLE_RATE * duration); byte[] pcm = new byte[numSamples * 2]; // 16-bit mono = 2 bytes per sample for (int i = 0; i < numSamples; i++) { double t = (double) i / SAMPLE_RATE; double sample; switch (waveType) { case SQUARE: sample = Math.signum(Math.sin(2.0 * Math.PI * frequency * t)); break; case SAWTOOTH: sample = 2.0 * (t * frequency - Math.floor(0.5 + t * frequency)); break; case SINE: default: sample = Math.sin(2.0 * Math.PI * frequency * t); break; } // Apply a short fade-in/fade-out envelope to avoid clicks int fadeLength = Math.min(numSamples / 10, SAMPLE_RATE / 200); if (i < fadeLength) { sample *= (double) i / fadeLength; } else if (i > numSamples - fadeLength) { sample *= (double) (numSamples - i) / fadeLength; } short pcmValue = (short) (sample * Short.MAX_VALUE); pcm[i * 2] = (byte) (pcmValue & 0xFF); pcm[i * 2 + 1] = (byte) ((pcmValue >> 8) & 0xFF); } return pcm; } }