aboutsummaryrefslogtreecommitdiff
path: root/pcmalsa.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'pcmalsa.cpp')
-rw-r--r--pcmalsa.cpp230
1 files changed, 230 insertions, 0 deletions
diff --git a/pcmalsa.cpp b/pcmalsa.cpp
new file mode 100644
index 0000000..eec9b94
--- /dev/null
+++ b/pcmalsa.cpp
@@ -0,0 +1,230 @@
+/*
+ * Copyright (C) 2022 Javier S. Pedro
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+#define LOG_ENABLED 1
+#define LOG_ENABLE_FLOW 1
+#define LOG_GROUP LOG_GROUP_DEV_SB16
+
+#include <VBox/err.h>
+#include <VBox/log.h>
+#include <alsa/asoundlib.h>
+#include "pcmalsa.h"
+
+PCMOutAlsa::PCMOutAlsa() : _pcm(NULL)
+{
+
+}
+
+PCMOutAlsa::~PCMOutAlsa()
+{
+}
+
+int PCMOutAlsa::open(const char *dev, unsigned int sampleRate, unsigned int channels)
+{
+ int err;
+
+ err = snd_pcm_open(&_pcm, dev, SND_PCM_STREAM_PLAYBACK, 0);
+ if (err < 0) {
+ LogWarn(("ALSA playback open error: %s\n", snd_strerror(err)));
+ return VERR_AUDIO_STREAM_COULD_NOT_CREATE;
+ }
+
+ // TODO: Right now, setting a too large period size means we will not let the main
+ // thread run long enough to actually change notes/voices. Need some actual synchronization.
+
+ int periodSize = 10 /*msec*/;
+ int bufferSize = 100 /*msec*/;
+
+ err = setParams(sampleRate, channels, bufferSize * 1000 /*usec*/, periodSize * 1000);
+ if (err < 0) {
+ snd_pcm_close(_pcm);
+ _pcm = NULL;
+ return VERR_AUDIO_STREAM_COULD_NOT_CREATE;
+ }
+
+ return VINF_SUCCESS;
+}
+
+int PCMOutAlsa::close()
+{
+ if (_pcm) {
+ snd_pcm_drain(_pcm);
+ snd_pcm_close(_pcm);
+ }
+ return VINF_SUCCESS;
+}
+
+ssize_t PCMOutAlsa::avail()
+{
+ snd_pcm_sframes_t frames = snd_pcm_avail(_pcm);
+ if (frames < 0) {
+ LogWarn(("ALSA trying to recover from avail error: %s\n", snd_strerror(frames)));
+ frames = snd_pcm_recover(_pcm, frames, 0);
+ if (frames == 0) {
+ frames = snd_pcm_avail(_pcm);
+ }
+ }
+ if (frames < 0) {
+ LogWarn(("ALSA avail error: %s\n", snd_strerror(frames)));
+ return VERR_AUDIO_STREAM_NOT_READY;
+ }
+
+ return frames;
+}
+
+int PCMOutAlsa::wait()
+{
+ int err = snd_pcm_wait(_pcm, -1);
+ if (err < 0) {
+ LogWarn(("ALSA trying to recover from wait error: %s\n", snd_strerror(err)));
+ err = snd_pcm_recover(_pcm, err, 0);
+ }
+ if (err < 0) {
+ LogWarn(("ALSA wait error: %s\n", snd_strerror(err)));
+ return VERR_AUDIO_STREAM_NOT_READY;
+ }
+ return VINF_SUCCESS;
+}
+
+
+ssize_t PCMOutAlsa::write(int16_t *buf, size_t n)
+{
+ snd_pcm_sframes_t frames = snd_pcm_writei(_pcm, buf, n);
+ if (frames < 0) {
+ LogFlow(("ALSA trying to recover from error: %s\n", snd_strerror(frames)));
+ frames = snd_pcm_recover(_pcm, frames, 0);
+ }
+ if (frames < 0) {
+ LogWarn(("ALSA write error: %s\n", snd_strerror(frames)));
+ return VERR_AUDIO_STREAM_NOT_READY;
+ }
+ return frames;
+}
+
+int PCMOutAlsa::setParams(unsigned int sampleRate, unsigned int channels, unsigned int bufferTime, unsigned int periodTime)
+{
+ snd_pcm_hw_params_t *hwparams;
+ snd_pcm_sw_params_t *swparams;
+ int err;
+
+ snd_pcm_hw_params_alloca(&hwparams);
+ snd_pcm_sw_params_alloca(&swparams);
+
+ /* choose all parameters */
+ err = snd_pcm_hw_params_any(_pcm, hwparams);
+ if (err < 0) {
+ LogWarnFunc(("Broken PCM configuration: no configurations available: %s", snd_strerror(err)));
+ return err;
+ }
+ /* set software resampling */
+ err = snd_pcm_hw_params_set_rate_resample(_pcm, hwparams, 1);
+ if (err < 0) {
+ LogWarnFunc(("Resampling setup failed: %s", snd_strerror(err)));
+ return err;
+ }
+ /* set the selected read/write format */
+ err = snd_pcm_hw_params_set_access(_pcm, hwparams, SND_PCM_ACCESS_RW_INTERLEAVED);
+ if (err < 0) {
+ LogWarnFunc(("Access type not available: %s", snd_strerror(err)));
+ return err;
+ }
+ /* set the sample format */
+ err = snd_pcm_hw_params_set_format(_pcm, hwparams, SND_PCM_FORMAT_S16);
+ if (err < 0) {
+ LogWarnFunc(("Sample format not available: %s", snd_strerror(err)));
+ return err;
+ }
+ /* set the count of channels */
+ err = snd_pcm_hw_params_set_channels(_pcm, hwparams, channels);
+ if (err < 0) {
+ LogWarnFunc(("Channels count (%i) not available: %s", channels, snd_strerror(err)));
+ return err;
+ }
+ /* set the stream rate */
+ unsigned int rrate = sampleRate;
+ err = snd_pcm_hw_params_set_rate_near(_pcm, hwparams, &rrate, 0);
+ if (err < 0) {
+ LogWarnFunc(("Rate %iHz not available for playback: %s", sampleRate, snd_strerror(err)));
+ return err;
+ }
+ if (rrate != sampleRate) {
+ LogWarnFunc(("Rate doesn't match (requested %iHz, get %iHz)", sampleRate, rrate));
+ return -EINVAL;
+ }
+
+ /* set the buffer time */
+ err = snd_pcm_hw_params_set_buffer_time_near(_pcm, hwparams, &bufferTime, NULL);
+ if (err < 0) {
+ LogWarnFunc(("Unable to set buffer time %u for playback: %s\n", bufferTime, snd_strerror(err)));
+ return err;
+ }
+ err = snd_pcm_hw_params_get_buffer_size(hwparams, &_bufferSize);
+ if (err < 0) {
+ printf("Unable to get buffer size for playback: %s\n", snd_strerror(err));
+ return err;
+ }
+
+ /* set the period time */
+ err = snd_pcm_hw_params_set_period_time_near(_pcm, hwparams, &periodTime, NULL);
+ if (err < 0) {
+ printf("Unable to set period time %u for playback: %s\n", periodTime, snd_strerror(err));
+ return err;
+ }
+ err = snd_pcm_hw_params_get_period_size(hwparams, &_periodSize, NULL);
+ if (err < 0) {
+ printf("Unable to get period size for playback: %s\n", snd_strerror(err));
+ return err;
+ }
+
+ /* write the parameters to device */
+ err = snd_pcm_hw_params(_pcm, hwparams);
+ if (err < 0) {
+ LogWarnFunc(("Unable to set hw params: %s", snd_strerror(err)));
+ return err;
+ }
+
+ LogFunc(("Using bufferSize=%lu periodSize=%lu\n", _bufferSize, _periodSize));
+
+ /* get the current swparams */
+ err = snd_pcm_sw_params_current(_pcm, swparams);
+ if (err < 0) {
+ LogWarnFunc(("Unable to determine current swparams: %s", snd_strerror(err)));
+ return err;
+ }
+ /* start the transfer when the buffer is almost full: */
+ /* (buffer_size / avail_min) * avail_min */
+ err = snd_pcm_sw_params_set_start_threshold(_pcm, swparams, (_bufferSize / _periodSize) * _periodSize);
+ if (err < 0) {
+ LogWarnFunc(("Unable to set start threshold mode: %s", snd_strerror(err)));
+ return err;
+ }
+ /* allow the transfer when at least period_size samples can be processed */
+ err = snd_pcm_sw_params_set_avail_min(_pcm, swparams, _periodSize);
+ if (err < 0) {
+ LogWarnFunc(("Unable to set avail min: %s", snd_strerror(err)));
+ return err;
+ }
+ /* write the parameters to the playback device */
+ err = snd_pcm_sw_params(_pcm, swparams);
+ if (err < 0) {
+ LogWarnFunc(("Unable to set sw params: %s", snd_strerror(err)));
+ return err;
+ }
+
+ return 0;
+}