public class PitchPresenter implements Presenter {

    private static final int RETRY_COUNT_MAX = 3;

    private final PitchView view;
    private final PitchPlayer pitchPlayer;
    private final VolumeObserver volumeObserver;

    private Disposable pitchPlayerDisposable;
    private Disposable volumeDisposable;

    private int retryCount = 0;

    public PitchPresenter(final PitchView view, final PitchPlayer pitchPlayer, final VolumeObserver volumeObserver) {
        this.view = view;
        this.pitchPlayer = pitchPlayer;
        this.volumeObserver = volumeObserver;
    }

    @Override
    public void detachView() {
        // No Operation
    }

    public void startPlayingNote(final double noteFrequency) {
        pitchPlayerDisposable = pitchPlayer.startPlaying(noteFrequency)
                .observeOn(AndroidSchedulers.mainThread())
                .subscribeOn(Schedulers.io())
                .subscribe(() -> {
                }, error -> {
                    Timber.e(error, "Error Starting Pitch Playback.");

                    boolean showAction = retryCount < RETRY_COUNT_MAX;
                    int description = showAction ? R.string.pitch_player_error_playing_note_description_with_action
                            : R.string.pitch_player_error_playing_note_description_without_action;

                    view.onErrorPlayingNote(
                            description,
                            showAction,
                            R.string.pitch_player_error_playing_note_action,
                            R.color.snack_bar_action_color
                    );
                });
    }

    public void retryPlayingNote(final double noteFrequency) {
        retryCount++;
        startPlayingNote(noteFrequency);
    }

    public void stopPlayingNote() {
        if (pitchPlayerDisposable != null) {
            pitchPlayerDisposable.dispose();
        }
    }

    public void startListeningToVolumeChanges() {
        volumeDisposable = volumeObserver.startListening()
                .observeOn(AndroidSchedulers.mainThread())
                .subscribeOn(Schedulers.io())
                .subscribe(state -> {
                    if (state == VolumeState.MUTED) {
                        view.onUpdateVolumeState(R.string.volume_state_muted, R.color.volume_muted);
                    } else {
                        view.onUpdateVolumeState(R.string.volume_state_on, R.color.volume_on);
                    }
                }, error -> {
                    Timber.e(error, "Error Starting to Listen to Volume Changes.");
                });
    }

    public void stopListeningToVolumeChanges() {
        if (volumeDisposable != null) {
            volumeDisposable.dispose();
        }
    }
} public class GuitarPitchPlayer implements PitchPlayer {

    private final AudioPlayer audioPlayer;
    private final FrequencyConverter frequencyConverter;

    public GuitarPitchPlayer(final AudioPlayer audioPlayer, final FrequencyConverter frequencyConverter) {
        this.audioPlayer = audioPlayer;
        this.frequencyConverter = frequencyConverter;
    }

    @Override
    public Completable startPlaying(double frequency) {

        return Completable.create(emitter -> {
            try {
                final float[] audioData = frequencyConverter.convert(frequency);

                audioPlayer.setBuffer(audioData);

                audioPlayer.play();
            } catch (Exception exception) {
                emitter.tryOnError(exception);
            }
        }).doOnEvent(event -> audioPlayer.stop())
                .doOnDispose(this::stopPlayingAndRelease);
    }

    private void stopPlayingAndRelease() {
        audioPlayer.stop();
        audioPlayer.release();
    }
} public interface PitchPlayer {

    /**
     * Starts playing the provided frequency and returns a {@link Completable} that performs the
     * playing of the frequency.
     *
     * @param frequency The frequency to be played.
     * @return A {@link Completable} running the playing operation until unsubscribed.
     */
    Completable startPlaying(double frequency);
} public class AndroidAudioConfig implements AudioConfig {

    private static final int AUDIO_SAMPLE_RATE = 44100;

    // Input Values
    private static final int AUDIO_RECORD_CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_DEFAULT;
    private static final int AUDIO_RECORD_AUDIO_FORMAT = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ? AudioFormat.ENCODING_PCM_FLOAT : AudioFormat.ENCODING_PCM_16BIT;
    private static final int AUDIO_RECORD_BUFFER_SIZE = AudioRecord.getMinBufferSize(AUDIO_SAMPLE_RATE,
            AUDIO_RECORD_CHANNEL_CONFIG, AUDIO_RECORD_AUDIO_FORMAT);
    private static final int AUDIO_RECORD_READ_SIZE = AUDIO_RECORD_BUFFER_SIZE / 4;
    private static final int AUDIO_RECORD_AUDIO_SOURCE = MediaRecorder.AudioSource.DEFAULT;

    // Output Values
    // Note, for some reason this only seems to work with stereo
    private static final int AUDIO_PLAYER_CHANNEL_CONFIG = AudioFormat.CHANNEL_OUT_STEREO;
    private static final int AUDIO_PLAYER_AUDIO_FORMAT = AudioFormat.ENCODING_PCM_FLOAT;
    // Note, this should probably be four (for 32-bit float) but it doesn't seem to work with that.
    private static final int AUDIO_PLAYER_AUDIO_FORMAT_BYTE_COUNT = 2;
    private static final int AUDIO_PLAYER_BUFFER_SIZE = AudioTrack.getMinBufferSize(AUDIO_SAMPLE_RATE,
            AUDIO_PLAYER_CHANNEL_CONFIG, AUDIO_PLAYER_AUDIO_FORMAT);
    private static final int AUDIO_PLAYER_WRITE_SIZE = AUDIO_PLAYER_BUFFER_SIZE / 4;

    public AndroidAudioConfig() {
        // Default constructor
    }

    @Override
    public int getSampleRate() {
        return AUDIO_SAMPLE_RATE;
    }

    @Override
    public int getInputBufferSize() {
        return AUDIO_RECORD_BUFFER_SIZE;
    }

    @Override
    public int getOutputBufferSize() {
        return AUDIO_PLAYER_BUFFER_SIZE;
    }

    @Override
    public int getReadSize() {
        return AUDIO_RECORD_READ_SIZE;
    }

    @Override
    public int getWriteSize() {
        return AUDIO_PLAYER_WRITE_SIZE;
    }

    @Override
    public int getInputChannel() {
        return AUDIO_RECORD_CHANNEL_CONFIG;
    }

    @Override
    public int getOutputChannel() {
        return AUDIO_PLAYER_CHANNEL_CONFIG;
    }

    @Override
    public int getInputFormat() {
        return AUDIO_RECORD_AUDIO_FORMAT;
    }

    @Override
    public int getOutputFormat() {
        return AUDIO_PLAYER_AUDIO_FORMAT;
    }

    @Override
    public int getOutputFormatByteCount() {
        return AUDIO_PLAYER_AUDIO_FORMAT_BYTE_COUNT;
    }

    @Override
    public int getInputSource() {
        return AUDIO_RECORD_AUDIO_SOURCE;
    }
} public interface AudioConfig {

    /**
     * The sample rate that the Audio Recorder is using, in hertz. Note that this value is device
     * dependent.
     *
     * @return The sample rate of the audio recording.
     */
    int getSampleRate();

    /**
     * The size of the underlying input waveform buffer array used by the Audio Recorder. This should be
     * larger than the value returned by {@link #getReadSize()}. This value should only be used to
     * create an instance of an Audio Recorder and when reading or performing operations on the
     * waveform buffer array, {@link #getReadSize()} should be used. Note that an implementation of
     * this method should return a valid value for the device.
     *
     * @return The size of the underlying waveform buffer array.
     */
    int getInputBufferSize();

    /**
     * The size of the underlying output waveform buffer array used by the Audio Recorder. This should be
     * larger than the value returned by {@link #getReadSize()}. This value should only be used to
     * create an instance of an Audio Recorder and when reading or performing operations on the
     * waveform buffer array, {@link #getReadSize()} should be used. Note that an implementation of
     * this method should return a valid value for the device.
     *
     * @return The size of the underlying waveform buffer array.
     */
    int getOutputBufferSize();

    /**
     * The size of the input array of data that should be read from the buffer. Use this value when
     * processing the buffer of waveform data.
     *
     * @return The size of values read from the underlying waveform buffer array.
     */
    int getReadSize();

    /**
     * The size of the array of data that should be written to the output buffer. Use this value when
     * processing the buffer of waveform data.
     *
     * @return The size of values written to the underlying waveform buffer array.
     */
    int getWriteSize();

    /**
     * The input channel to retrieve the input data.
     *
     * @return The input channel
     */
    int getInputChannel();

    /**
     * The output channel to output the data. For example, a value representing Stereo.
     *
     * @return The output channel.
     */
    int getOutputChannel();

    /**
     * The input audio format of the buffer array (ex: float or short).
     *
     * @return The format of the buffer array.
     */
    int getInputFormat();

    /**
     * The output audio format of the buffer array (ex: float or short).
     *
     * @return The format of the buffer array.
     */
    int getOutputFormat();

    /**
     * The amount of bytes to represent a single piece of output data. For instance, 16-bit PCM data
     * can be represented by two bytes.
     *
     * @return The amount of bytes needed to represent the output data.
     */
    int getOutputFormatByteCount();

    /**
     * The input source of the audio data.
     *
     * @return The input source.
     */
    int getInputSource();
} public interface FrequencyConverter {

    /**
     * Converts the provided frequency value into a float array representing the audio waveform
     * data with values in the range [-1, 1].
     *
     * @param frequency The provided frequency to convert.
     * @return The audio waveform buffer representing the provided frequency.
     */
    float[] convert(double frequency);
} public class SineWaveFrequencyConverter implements FrequencyConverter {

    private static final double TWO_PI = 2 * Math.PI;

    private final int sampleRate;
    private final int initialWriteBufferSize;
    private final int outputFormatByteCount;

    public SineWaveFrequencyConverter(final AudioConfig audioConfig) {
        sampleRate = audioConfig.getSampleRate();
        initialWriteBufferSize = audioConfig.getWriteSize();
        outputFormatByteCount = audioConfig.getOutputFormatByteCount();
    }

    @Override
    public float[] convert(final double frequency) {
        // How many sine wave cycles of the frequency can occur for the sample rate
        // This can also be viewed as the wavelength (amount of indices for one complete cycle) within the resulting buffer
        double cyclesPerSampleRate = sampleRate / frequency;

        // How many sine wave cycles of the frequency can occur for the output buffer
        int cyclesPerBuffer = (int) (initialWriteBufferSize / cyclesPerSampleRate);

        // The largest, rounded amount of cycles that can fit in the initial buffer that is also divisible by the output format byte count
        int roundedCyclesPerBuffer = (cyclesPerBuffer / outputFormatByteCount) * outputFormatByteCount;

        // The final output buffer size
        int outputBufferSize = (int) (roundedCyclesPerBuffer * cyclesPerSampleRate);

        // The total rounding deviation that will occur over the course of the buffer
        double entireBufferDeviation = (roundedCyclesPerBuffer * cyclesPerSampleRate) % ((int) cyclesPerSampleRate);

        // The difference that should be applied to each buffer value to counter the total deviation and avoid "click" sounds
        double deviationDiff = entireBufferDeviation / outputBufferSize;

        float[] audioData = new float[outputBufferSize];

        for (int i = 0; i < audioData.length; i++) {
            // The y value of the sine wave at the specific buffer index
            // y(x) = sin(2 * PI * x / sampleRate / frequency) or
            // y(x) = sin(2 * PI * x * frequency / sampleRate)
            audioData[i] = (float) (Math.sin(TWO_PI * i / cyclesPerSampleRate) - deviationDiff);
        }

        return audioData;
    }
} public class AndroidAudioPlayer implements AudioPlayer {

    private static final int LOOP_COUNT_INFINITE = -1;

    private final AudioTrack audioTrack;

    private final int outputByteCount;

    public AndroidAudioPlayer(final AudioConfig audioConfig) {
        AudioAttributes audioAttributes = new AudioAttributes.Builder()
                .setLegacyStreamType(AudioManager.STREAM_MUSIC)
                .setUsage(AudioAttributes.USAGE_MEDIA)
                .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
                .build();

        AudioFormat audioFormat = new AudioFormat.Builder()
                .setChannelMask(audioConfig.getOutputChannel())
                .setEncoding(audioConfig.getOutputFormat())
                .setSampleRate(audioConfig.getSampleRate())
                .build();

        audioTrack = new AudioTrack(audioAttributes,
                audioFormat,
                audioConfig.getOutputBufferSize(),
                AudioTrack.MODE_STATIC,
                AudioManager.AUDIO_SESSION_ID_GENERATE);

        outputByteCount = audioConfig.getOutputFormatByteCount();
    }

    @Override
    public void play() {
        audioTrack.reloadStaticData();
        audioTrack.play();
    }

    @Override
    public void stop() {
        audioTrack.pause();
        audioTrack.flush();
        audioTrack.stop();
    }

    @Override
    public void release() {
        audioTrack.release();
    }

    @Override
    public void setBuffer(final float[] waveformBuffer) {
        audioTrack.write(waveformBuffer, 0, waveformBuffer.length, AudioTrack.WRITE_BLOCKING);
        audioTrack.setLoopPoints(0, waveformBuffer.length / outputByteCount, LOOP_COUNT_INFINITE);
    }
} public interface AudioPlayer {

    /**
     * Starts playing the buffer provided in the {@link #setBuffer(float[])} method to the audio output.
     */
    void play();

    /**
     * Stops playing to the audio output the buffer provided in the {@link #setBuffer(float[])} method.
     */
    void stop();

    /**
     * Releases all the internal resources held by this AudioPlayer. This should be called when the
     * AudioPlayer is no longer needed and after a call to {@link #stop()}. After this method is
     * called, a new AudioPlayer will have to be created to play again.
     */
    void release();

    /**
     * Sets the audio waveform data that will be played to the audio output.
     *
     * @param waveformBuffer The waveform data to be played.
     */
    void setBuffer(float waveformBuffer[]);
} public class AndroidVolumeObserver implements VolumeObserver {

    private static final long INITIAL_DELAY = 0;
    private static final long INTERVAL = 3000;
    private static final TimeUnit UNIT = TimeUnit.MILLISECONDS;

    private final Observable<VolumeState> observable;

    public AndroidVolumeObserver(final AudioManager audioManager) {
        observable = Observable.interval(INITIAL_DELAY, INTERVAL, UNIT)
                .map(tick -> VolumeState.forVolume(audioManager.getStreamVolume(AudioManager.STREAM_MUSIC)));
    }

    @Override
    public Observable<VolumeState> startListening() {
        return observable.share();
    }
} public interface VolumeObserver {

    /**
     * Subscribes to the volume observer for volume states, either on an interval or changes to the
     * devices volume state.
     *
     * @return An {@link Observable<VolumeState>} providing the volume states as long as subscribed.
     */
    Observable<VolumeState> startListening();
} public interface PitchView {

    void onUpdateVolumeState(@StringRes int volumeStateText, @ColorRes int textColor);

    void onErrorPlayingNote(@StringRes int errorDescription, boolean showAction, @StringRes int errorAction, @ColorRes int actionColor);
}
import android.content.Context;
import android.media.AudioManager;
import com.chrynan.android_guitar_tuner.di.ApplicationContext;
import com.chrynan.android_guitar_tuner.di.FragmentScope;
import com.chrynan.android_guitar_tuner.presenter.PitchPresenter;
import com.chrynan.android_guitar_tuner.tuner.GuitarPitchPlayer;
import com.chrynan.android_guitar_tuner.tuner.PitchPlayer;
import com.chrynan.android_guitar_tuner.tuner.config.AndroidAudioConfig;
import com.chrynan.android_guitar_tuner.tuner.config.AudioConfig;
import com.chrynan.android_guitar_tuner.tuner.converter.FrequencyConverter;
import com.chrynan.android_guitar_tuner.tuner.converter.SineWaveFrequencyConverter;
import com.chrynan.android_guitar_tuner.tuner.player.AndroidAudioPlayer;
import com.chrynan.android_guitar_tuner.tuner.player.AudioPlayer;
import com.chrynan.android_guitar_tuner.tuner.volume.AndroidVolumeObserver;
import com.chrynan.android_guitar_tuner.tuner.volume.VolumeObserver;
import com.chrynan.android_guitar_tuner.ui.view.PitchView;
import dagger.Module;
import dagger.Provides;
package com.chrynan.android_guitar_tuner.di.module;




/**
 * A Dagger {@link Module} used for dependency injection in a {@link PitchView} implementation.
 */
@Module
public class PitchViewModule {

    private final PitchView view;

    public PitchViewModule(final PitchView view) {
        this.view = view;
    }

    @Provides
    @FragmentScope
    AudioManager provideAudioManager(@ApplicationContext final Context context) {
        return (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
    }

    @Provides
    @FragmentScope
    AudioConfig provideAudioConfig() {
        return new AndroidAudioConfig();
    }

    @Provides
    @FragmentScope
    AudioPlayer provideAudioPlayer(final AudioConfig audioConfig) {
        return new AndroidAudioPlayer(audioConfig);
    }

    @Provides
    @FragmentScope
    FrequencyConverter provideFrequencyConverter(final AudioConfig audioConfig) {
        return new SineWaveFrequencyConverter(audioConfig);
    }

    @Provides
    @FragmentScope
    PitchPlayer providePitchPlayer(final AudioPlayer audioPlayer, final FrequencyConverter frequencyConverter) {
        return new GuitarPitchPlayer(audioPlayer, frequencyConverter);
    }

    @Provides
    @FragmentScope
    VolumeObserver provideVolumeObserver(final AudioManager audioManager) {