Skip to content

Instantly share code, notes, and snippets.

@pamaury
Created March 12, 2018 13:57
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save pamaury/dbc4f1f54f333c0ff04d8d6e6ce22238 to your computer and use it in GitHub Desktop.
/***************************************************************************
* __________ __ ___.
* Open \______ \ ____ ____ | | _\_ |__ _______ ___
* Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ /
* Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < <
* Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \
* \/ \/ \/ \/ \/
*
* Copyright (C) 2010 Thomas Martitz
* Copyright (C) 2018 Amaury Pouly
*
* 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 software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
* KIND, either express or implied.
*
****************************************************************************/
#include <config.h>
#include <stddef.h>
#include <stdbool.h>
#include <alsa/asoundlib.h>
#include "system.h"
#include "pcm.h"
#include "pcm-internal.h"
#include "pcm_mixer.h"
#include "pcm_sampr.h"
#include "audiohw.h"
#include "pcm-alsa.h"
#include "panic.h"
#include "debug.h"
#include <pthread.h>
static char device[] = "default"; /* playback device */
static const snd_pcm_access_t access_ = SND_PCM_ACCESS_RW_INTERLEAVED; /* access mode */
#ifdef SONY_NWZ_LINUX
/* Sony NWZ must use 32-bit per sample */
static const snd_pcm_format_t format = SND_PCM_FORMAT_S32_LE; /* sample format */
typedef int32_t sample_t;
#else
static const snd_pcm_format_t format = SND_PCM_FORMAT_S16; /* sample format */
typedef int16_t sample_t;
#endif
static const int channels = 2; /* count of channels */
static unsigned int rate = 44100; /* stream rate */
static snd_pcm_t *handle;
static snd_pcm_sframes_t buffer_size = MIX_FRAME_SAMPLES * 32; /* ~16k */
static snd_pcm_sframes_t period_size = MIX_FRAME_SAMPLES * 4; /* ~4k */
static sample_t *frames;
/* buffer currently being played (always updated by the thread) */
static const void *pcm_data = 0;
static size_t pcm_size = 0;
/* synchronization variables between rockbox and pcm thread */
static volatile bool dma_stopped;
static volatile bool dma_locked;
static volatile bool dma_quit;
static pthread_mutex_t dma_mutex = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t dma_wakeup = PTHREAD_COND_INITIALIZER; /* protected by mutex */
static volatile int dma_play_lock; /* protected by mutex */
static pthread_t dma_thread;
/* request for new buffer to be played (replaces currently being played buffer) */
static const void *pcm_new_data = 0; /* protected by mutex */
static size_t pcm_new_size = 0; /* protected by mutex */
static int set_hwparams(snd_pcm_t *handle, unsigned sample_rate)
{
unsigned int rrate;
int err;
snd_pcm_hw_params_t *params;
snd_pcm_hw_params_malloc(&params);
/* choose all parameters */
err = snd_pcm_hw_params_any(handle, params);
if (err < 0)
{
printf("Broken configuration for playback: no configurations available: %s\n", snd_strerror(err));
goto error;
}
/* set the interleaved read/write format */
err = snd_pcm_hw_params_set_access(handle, params, access_);
if (err < 0)
{
printf("Access type not available for playback: %s\n", snd_strerror(err));
goto error;
}
/* set the sample format */
err = snd_pcm_hw_params_set_format(handle, params, format);
if (err < 0)
{
printf("Sample format not available for playback: %s\n", snd_strerror(err));
goto error;
}
/* set the count of channels */
err = snd_pcm_hw_params_set_channels(handle, params, channels);
if (err < 0)
{
printf("Channels count (%i) not available for playbacks: %s\n", channels, snd_strerror(err));
goto error;
}
/* set the stream rate */
rrate = sample_rate;
err = snd_pcm_hw_params_set_rate_near(handle, params, &rrate, 0);
if (err < 0)
{
printf("Rate %iHz not available for playback: %s\n", rate, snd_strerror(err));
goto error;
}
if (rrate != sample_rate)
{
printf("Rate doesn't match (requested %iHz, get %iHz)\n", sample_rate, err);
err = -EINVAL;
goto error;
}
/* set the buffer size */
err = snd_pcm_hw_params_set_buffer_size_near(handle, params, &buffer_size);
if (err < 0)
{
printf("Unable to set buffer size %ld for playback: %s\n", buffer_size, snd_strerror(err));
goto error;
}
/* set the period size */
err = snd_pcm_hw_params_set_period_size_near (handle, params, &period_size, NULL);
if (err < 0)
{
printf("Unable to set period size %ld for playback: %s\n", period_size, snd_strerror(err));
goto error;
}
if (!frames)
frames = malloc(period_size * channels * sizeof(sample_t));
/* write the parameters to device */
err = snd_pcm_hw_params(handle, params);
if (err < 0)
{
printf("Unable to set hw params for playback: %s\n", snd_strerror(err));
goto error;
}
err = 0; /* success */
error:
snd_pcm_hw_params_free(params);
return err;
}
/* Set sw params: playback start threshold and low buffer watermark */
static int set_swparams(snd_pcm_t *handle)
{
int err;
snd_pcm_sw_params_t *swparams;
snd_pcm_sw_params_malloc(&swparams);
/* get the current swparams */
err = snd_pcm_sw_params_current(handle, swparams);
if (err < 0)
{
printf("Unable to determine current swparams for playback: %s\n", snd_strerror(err));
goto error;
}
/* start the transfer when the buffer is half full */
err = snd_pcm_sw_params_set_start_threshold(handle, swparams, buffer_size / 2);
if (err < 0)
{
printf("Unable to set start threshold mode for playback: %s\n", snd_strerror(err));
goto error;
}
/* allow the transfer when at least period_size samples can be processed */
err = snd_pcm_sw_params_set_avail_min(handle, swparams, period_size);
if (err < 0)
{
printf("Unable to set avail min for playback: %s\n", snd_strerror(err));
goto error;
}
/* write the parameters to the playback device */
err = snd_pcm_sw_params(handle, swparams);
if (err < 0)
{
printf("Unable to set sw params for playback: %s\n", snd_strerror(err));
goto error;
}
err = 0; /* success */
error:
snd_pcm_sw_params_free(swparams);
return err;
}
/* Digital volume explanation:
* with very good approximation (<0.1dB) the convertion from dB to multiplicative
* factor, for dB>=0, is 2^(dB/3). We can then notice that if we write dB=3*k+r
* then this is 2^k*2^(r/3) so we only need to look at r=0,1,2. For r=0 this is
* 1, for r=1 we have 2^(1/3)~=1.25 so we approximate by 1+1/4, and 2^(2/3)~=1.5
* so we approximate by 1+1/2. To go from negative to nonnegative we notice that
* 48 dB => 63095 factor ~= 2^16 so we virtually pre-multiply everything by 2^(-16)
* and add 48dB to the input volume. We cannot go lower -43dB because several
* values between -48dB and -43dB would require a fractional multiplier, which is
* stupid to implement for such very low volume. */
static int dig_vol_mult = 2 << 16; /* multiplicative factor to apply to each sample */
void pcm_set_mixer_volume(int vol_db)
{
if(vol_db > 0 || vol_db < -43)
panicf("invalid pcm alsa volume");
if(format != SND_PCM_FORMAT_S32_LE)
panicf("this function assumes 32-bit sample size");
vol_db += 48; /* -42dB .. 0dB => 5dB .. 48dB */
/* NOTE if vol_dB = 5 then vol_shift = 1 but r = 1 so we do vol_shift - 1 >= 0
* otherwise vol_dB >= 0 implies vol_shift >= 2 so vol_shift - 2 >= 0 */
int vol_shift = vol_db / 3;
int r = vol_db % 3;
if(r == 0)
dig_vol_mult = 1 << vol_shift;
else if(r == 1)
dig_vol_mult = 1 << vol_shift | 1 << (vol_shift - 2);
else
dig_vol_mult = 1 << vol_shift | 1 << (vol_shift - 1);
DEBUGF("%d dB -> factor = %d\n", vol_db - 48, dig_vol_mult);
}
/* copy pcm samples to the frame buffer, filling an entire period (padded with silence if necessary)
* returns true on success, false is there is nothing to play */
static bool fill_frames(void)
{
ssize_t copy_n, frames_left = period_size;
bool new_buffer = false;
while (frames_left > 0)
{
if (!pcm_size)
{
new_buffer = true;
/* FIXME: we should check dma_locked here, probably by holding the mutex first */
if (dma_locked)
DEBUGF("pcm dma: ouch\n");
if (!pcm_play_dma_complete_callback(PCM_DMAST_OK, &pcm_data,
&pcm_size))
{
return false;
}
}
if (pcm_size % PCM_SAMPLE_SIZE)
panicf("Wrong pcm_size");
/* the compiler will optimize this test away */
copy_n = MIN((ssize_t)(pcm_size / PCM_SAMPLE_SIZE), frames_left);
if (format == SND_PCM_FORMAT_S32_LE)
{
/* We have to convert 16-bit to 32-bit, the need to multiply the
* sample by some value so the sound is not too low */
const int16_t *pcm_ptr = pcm_data;
sample_t *sample_ptr = &frames[2*(period_size-frames_left)];
for (int i = 0; i < copy_n*2; i++)
*sample_ptr++ = *pcm_ptr++ * dig_vol_mult;
}
else
{
/* Rockbox and PCM have same format: memcopy */
memcpy(&frames[2*(period_size-frames_left)], pcm_data, copy_n * 4);
}
pcm_data += copy_n * PCM_SAMPLE_SIZE;
pcm_size -= copy_n * PCM_SAMPLE_SIZE;
frames_left -= copy_n;
if (new_buffer)
{
new_buffer = false;
pcm_play_dma_status_callback(PCM_DMAST_STARTED);
}
}
return true;
}
static void handle_xrun(void)
{
panicf("pcm dma: xrun");
}
static void handle_suspend(void)
{
panicf("pcm dma: suspend");
}
static const char *pcm_state_str(snd_pcm_state_t state)
{
switch (state)
{
case SND_PCM_STATE_RUNNING: return "SND_PCM_STATE_RUNNING";
case SND_PCM_STATE_XRUN: return "SND_PCM_STATE_XRUN";
case SND_PCM_STATE_SETUP: return "SND_PCM_STATE_SETUP";
case SND_PCM_STATE_PREPARED: return "SND_PCM_STATE_PREPARED";
case SND_PCM_STATE_PAUSED: return "SND_PCM_STATE_PAUSED";
case SND_PCM_STATE_DRAINING: return "SND_PCM_STATE_DRAINING";
default: return "????";
}
}
static void *pcm_dma_thread(void *arg)
{
(void) arg;
_logf("pcm dma thread");
while (true)
{
pthread_mutex_lock(&dma_mutex);
while ((dma_stopped || dma_locked) && !dma_quit)
{
DEBUGF("pcm dma: playback suspended (stopped=%d, locked=%d)\n", dma_stopped, dma_locked);
pthread_cond_wait(&dma_wakeup, &dma_mutex);
DEBUGF("pcm dma: playback resumed\n");
}
if(dma_quit)
break;
/* check for request to change buffer */
if (pcm_new_data)
{
pcm_data = pcm_new_data;
pcm_size = pcm_new_size;
pcm_new_data = NULL;
pcm_new_size = 0;
DEBUGF("pcm dma: new buffer request (ptr=%p, sz=%zd)\n", pcm_data, pcm_size);
}
pthread_mutex_unlock(&dma_mutex);
if (!fill_frames())
{
DEBUGF("pcm dma: no audio data\n");
/* at this point, the pcm code should have called pcm_play_dma_stop() so will we suspend */
continue;
}
size_t count = period_size;
sample_t *frames_ptr = frames;
while(count > 0 && !dma_stopped && !dma_quit)
{
int r = snd_pcm_writei(handle, frames_ptr, count);
if(r <= 0)
_logf("pcm dma: writei(%p, %zd) = %d\n", frames_ptr, count, r);
if (r >= 0)
{
frames_ptr += r * 2;
count -= r;
}
else if(r == -EAGAIN)
{
DEBUGF("pcm dma: wait a bit (EAGAIN)\n");
snd_pcm_wait(handle, 1);
}
else if(r == -EPIPE)
handle_xrun();
else if(r == -ESTRPIPE)
handle_suspend();
}
}
DEBUGF("pcm dma: quit\n");
return 0;
}
void cleanup(void)
{
/* since the dma thread is running concurrently, make sure that it is stopped before we start
* freeing any resource */
pthread_mutex_lock(&dma_mutex);
dma_quit = true;
pthread_cond_signal(&dma_wakeup);
pthread_mutex_unlock(&dma_mutex);
DEBUGF("waiting for pcm dma thread to finish\n");
pthread_join(dma_thread, NULL);
free(frames);
frames = NULL;
snd_pcm_close(handle);
}
void pcm_play_dma_init(void)
{
int err;
audiohw_preinit();
if ((err = snd_pcm_open(&handle, device, SND_PCM_STREAM_PLAYBACK, 0)) < 0)
panicf("%s(): Cannot open device %s: %s\n", __func__, device, snd_strerror(err));
if ((err = set_hwparams(handle, rate)) < 0)
panicf("Setting of hwparams failed: %s\n", snd_strerror(err));
if ((err = set_swparams(handle)) < 0)
panicf("Setting of swparams failed: %s\n", snd_strerror(err));
pcm_dma_apply_settings();
/* make sure the pcm thread starts suspended */
dma_stopped = true;
pthread_create(&dma_thread, NULL, pcm_dma_thread, NULL);
atexit(cleanup);
}
void pcm_play_lock(void)
{
if (++dma_play_lock == 1)
{
pthread_mutex_lock(&dma_mutex);
dma_locked = true;
pthread_mutex_unlock(&dma_mutex);
}
}
void pcm_play_unlock(void)
{
if (--dma_play_lock == 0)
{
pthread_mutex_lock(&dma_mutex);
dma_locked = false;
/* wakup thread in case it is waiting to get the lock */
pthread_cond_signal(&dma_wakeup);
pthread_mutex_unlock(&dma_mutex);
}
}
static void pcm_dma_apply_settings_nolock(void)
{
snd_pcm_drop(handle);
set_hwparams(handle, pcm_sampr);
#if defined(HAVE_NWZ_LINUX_CODEC)
/* Sony NWZ linux driver uses a nonstandard mecanism to set the sampling rate */
audiohw_set_frequency(pcm_sampr);
#endif
}
void pcm_dma_apply_settings(void)
{
pcm_play_lock();
pcm_dma_apply_settings_nolock();
pcm_play_unlock();
}
void pcm_play_dma_pause(bool pause)
{
panicf("dma pause");
snd_pcm_pause(handle, pause);
}
void pcm_play_dma_stop(void)
{
DEBUGF("pcm_play_dma_stop()\n");
pthread_mutex_lock(&dma_mutex);
/* violently drop all pending frames */
snd_pcm_drop(handle);
dma_stopped = true;
pthread_mutex_unlock(&dma_mutex);
}
void pcm_play_dma_start(const void *addr, size_t size)
{
DEBUGF("pcm_play_dma_start()\n");
pcm_dma_apply_settings_nolock();
pthread_mutex_lock(&dma_mutex);
pcm_new_data = addr;
pcm_new_size = size;
dma_stopped = false;
pthread_cond_signal(&dma_wakeup);
pthread_mutex_unlock(&dma_mutex);
}
size_t pcm_get_bytes_waiting(void)
{
return pcm_size;
}
const void *pcm_play_dma_get_peak_buffer(int *count)
{
uintptr_t addr = (uintptr_t)pcm_data;
*count = pcm_size / 4;
return (void *)((addr + 3) & ~3);
}
void pcm_play_dma_postinit(void)
{
audiohw_postinit();
}
#ifdef HAVE_RECORDING
void pcm_rec_lock(void)
{
}
void pcm_rec_unlock(void)
{
}
void pcm_rec_dma_init(void)
{
}
void pcm_rec_dma_close(void)
{
}
void pcm_rec_dma_start(void *start, size_t size)
{
(void)start;
(void)size;
}
void pcm_rec_dma_stop(void)
{
}
const void *pcm_rec_dma_get_peak_buffer(void)
{
return NULL;
}
#endif /* HAVE_RECORDING */
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment