| /*************************************************************************** |
| * __________ __ ___. |
| * Open \______ \ ____ ____ | | _\_ |__ _______ ___ |
| * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / |
| * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < |
| * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ |
| * \/ \/ \/ \/ \/ |
| * $Id$ |
| * |
| * Copyright (C) 2007 by Michael Sevakis |
| * |
| * 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 <stdlib.h> |
| #include "system.h" |
| #include "kernel.h" |
| |
| /* Define LOGF_ENABLE to enable logf output in this file */ |
| /*#define LOGF_ENABLE*/ |
| #include "logf.h" |
| #include "audio.h" |
| #include "sound.h" |
| #include "general.h" |
| #include "pcm-internal.h" |
| #include "pcm_mixer.h" |
| |
| /** |
| * Aspects implemented in the target-specific portion: |
| * |
| * ==Playback== |
| * Public - |
| * pcm_postinit |
| * pcm_get_bytes_waiting |
| * pcm_play_lock |
| * pcm_play_unlock |
| * Semi-private - |
| * pcm_play_get_more_callback |
| * pcm_play_dma_init |
| * pcm_play_dma_postinit |
| * pcm_play_dma_start |
| * pcm_play_dma_stop |
| * pcm_play_dma_pause |
| * pcm_play_dma_get_peak_buffer |
| * Data Read/Written within TSP - |
| * pcm_sampr (R) |
| * pcm_fsel (R) |
| * pcm_curr_sampr (R) |
| * pcm_playing (R) |
| * pcm_paused (R) |
| * |
| * ==Playback/Recording== |
| * Public - |
| * pcm_dma_addr |
| * Semi-private - |
| * pcm_dma_apply_settings |
| * |
| * ==Recording== |
| * Public - |
| * pcm_rec_lock |
| * pcm_rec_unlock |
| * Semi-private - |
| * pcm_rec_more_ready_callback |
| * pcm_rec_dma_init |
| * pcm_rec_dma_close |
| * pcm_rec_dma_start |
| * pcm_rec_dma_stop |
| * pcm_rec_dma_get_peak_buffer |
| * Data Read/Written within TSP - |
| * pcm_recording (R) |
| * |
| * States are set _after_ the target's pcm driver is called so that it may |
| * know from whence the state is changed. One exception is init. |
| * |
| */ |
| |
| /* 'true' when all stages of pcm initialization have completed */ |
| static bool pcm_is_ready = false; |
| |
| /* the registered callback function to ask for more mp3 data */ |
| static pcm_play_callback_type pcm_callback_for_more SHAREDBSS_ATTR = NULL; |
| void (* pcm_play_dma_started)(void) SHAREDBSS_ATTR = NULL; |
| /* PCM playback state */ |
| volatile bool pcm_playing SHAREDBSS_ATTR = false; |
| /* PCM paused state. paused implies playing */ |
| volatile bool pcm_paused SHAREDBSS_ATTR = false; |
| /* samplerate of currently playing audio - undefined if stopped */ |
| unsigned long pcm_curr_sampr SHAREDBSS_ATTR = 0; |
| /* samplerate waiting to be set */ |
| unsigned long pcm_sampr SHAREDBSS_ATTR = HW_SAMPR_DEFAULT; |
| /* samplerate frequency selection index */ |
| int pcm_fsel SHAREDBSS_ATTR = HW_FREQ_DEFAULT; |
| |
| /* peak data for the global peak values - i.e. what the final output is */ |
| static struct pcm_peaks global_peaks; |
| |
| /* Called internally by functions to reset the state */ |
| static void pcm_play_stopped(void) |
| { |
| pcm_callback_for_more = NULL; |
| pcm_play_dma_started = NULL; |
| pcm_paused = false; |
| pcm_playing = false; |
| } |
| |
| static void pcm_wait_for_init(void) |
| { |
| while (!pcm_is_ready) |
| sleep(0); |
| } |
| |
| /** |
| * Perform peak calculation on a buffer of packed 16-bit samples. |
| * |
| * Used for recording and playback. |
| */ |
| static void pcm_peak_peeker(const int32_t *addr, int count, uint16_t peaks[2]) |
| { |
| int peak_l = 0, peak_r = 0; |
| const int32_t * const end = addr + count; |
| |
| do |
| { |
| int32_t value = *addr; |
| int ch; |
| |
| #ifdef ROCKBOX_BIG_ENDIAN |
| ch = value >> 16; |
| #else |
| ch = (int16_t)value; |
| #endif |
| if (ch < 0) |
| ch = -ch; |
| if (ch > peak_l) |
| peak_l = ch; |
| |
| #ifdef ROCKBOX_BIG_ENDIAN |
| ch = (int16_t)value; |
| #else |
| ch = value >> 16; |
| #endif |
| if (ch < 0) |
| ch = -ch; |
| if (ch > peak_r) |
| peak_r = ch; |
| |
| addr += 4; |
| } |
| while (addr < end); |
| |
| peaks[0] = peak_l; |
| peaks[1] = peak_r; |
| } |
| |
| void pcm_do_peak_calculation(struct pcm_peaks *peaks, bool active, |
| const void *addr, int count) |
| { |
| long tick = current_tick; |
| |
| /* Peak no farther ahead than expected period to avoid overcalculation */ |
| long period = tick - peaks->tick; |
| |
| /* Keep reasonable limits on period */ |
| if (period < 1) |
| period = 1; |
| else if (period > HZ/5) |
| period = HZ/5; |
| |
| peaks->period = (3*peaks->period + period) >> 2; |
| peaks->tick = tick; |
| |
| if (active) |
| { |
| int framecount = peaks->period*pcm_curr_sampr / HZ; |
| count = MIN(framecount, count); |
| |
| if (count > 0) |
| pcm_peak_peeker((int32_t *)addr, count, peaks->val); |
| /* else keep previous peak values */ |
| } |
| else |
| { |
| /* peaks are zero */ |
| peaks->val[0] = peaks->val[1] = 0; |
| } |
| } |
| |
| void pcm_calculate_peaks(int *left, int *right) |
| { |
| int count; |
| const void *addr = pcm_play_dma_get_peak_buffer(&count); |
| |
| pcm_do_peak_calculation(&global_peaks, pcm_playing && !pcm_paused, |
| addr, count); |
| |
| if (left) |
| *left = global_peaks.val[0]; |
| |
| if (right) |
| *right = global_peaks.val[1]; |
| } |
| |
| const void* pcm_get_peak_buffer(int * count) |
| { |
| return pcm_play_dma_get_peak_buffer(count); |
| } |
| |
| bool pcm_is_playing(void) |
| { |
| return pcm_playing; |
| } |
| |
| bool pcm_is_paused(void) |
| { |
| return pcm_paused; |
| } |
| |
| /**************************************************************************** |
| * Functions that do not require targeted implementation but only a targeted |
| * interface |
| */ |
| |
| /* This should only be called at startup before any audio playback or |
| recording is attempted */ |
| void pcm_init(void) |
| { |
| logf("pcm_init"); |
| |
| pcm_play_stopped(); |
| |
| pcm_set_frequency(HW_SAMPR_DEFAULT); |
| |
| logf(" pcm_play_dma_init"); |
| pcm_play_dma_init(); |
| } |
| |
| /* Finish delayed init */ |
| void pcm_postinit(void) |
| { |
| logf("pcm_postinit"); |
| |
| logf(" pcm_play_dma_postinit"); |
| |
| pcm_play_dma_postinit(); |
| |
| pcm_is_ready = true; |
| } |
| |
| bool pcm_is_initialized(void) |
| { |
| return pcm_is_ready; |
| } |
| |
| /* Common code to pcm_play_data and pcm_play_pause */ |
| static void pcm_play_data_start(unsigned char *start, size_t size) |
| { |
| ALIGN_AUDIOBUF(start, size); |
| |
| if (!(start && size)) |
| { |
| pcm_play_callback_type get_more = pcm_callback_for_more; |
| size = 0; |
| if (get_more) |
| { |
| logf(" get_more"); |
| get_more(&start, &size); |
| ALIGN_AUDIOBUF(start, size); |
| } |
| } |
| |
| if (start && size) |
| { |
| logf(" pcm_play_dma_start"); |
| pcm_apply_settings(); |
| pcm_play_dma_start(start, size); |
| pcm_playing = true; |
| pcm_paused = false; |
| return; |
| } |
| |
| /* Force a stop */ |
| logf(" pcm_play_dma_stop"); |
| pcm_play_dma_stop(); |
| pcm_play_stopped(); |
| } |
| |
| void pcm_play_data(pcm_play_callback_type get_more, |
| unsigned char *start, size_t size) |
| { |
| logf("pcm_play_data"); |
| |
| pcm_play_lock(); |
| |
| pcm_callback_for_more = get_more; |
| |
| logf(" pcm_play_data_start"); |
| pcm_play_data_start(start, size); |
| |
| pcm_play_unlock(); |
| } |
| |
| void pcm_play_get_more_callback(void **start, size_t *size) |
| { |
| pcm_play_callback_type get_more = pcm_callback_for_more; |
| |
| *size = 0; |
| |
| if (get_more && start) |
| { |
| /* Call registered callback */ |
| get_more((unsigned char **)start, size); |
| |
| ALIGN_AUDIOBUF(*start, *size); |
| |
| if (*start && *size) |
| return; |
| } |
| |
| /* Error, callback missing or no more DMA to do */ |
| pcm_play_dma_stop(); |
| pcm_play_stopped(); |
| } |
| |
| void pcm_play_pause(bool play) |
| { |
| logf("pcm_play_pause: %s", play ? "play" : "pause"); |
| |
| pcm_play_lock(); |
| |
| if (play == pcm_paused && pcm_playing) |
| { |
| if (!play) |
| { |
| logf(" pcm_play_dma_pause"); |
| pcm_play_dma_pause(true); |
| pcm_paused = true; |
| } |
| else if (pcm_get_bytes_waiting() > 0) |
| { |
| logf(" pcm_play_dma_pause"); |
| pcm_apply_settings(); |
| pcm_play_dma_pause(false); |
| pcm_paused = false; |
| } |
| else |
| { |
| logf(" pcm_play_dma_start: no data"); |
| pcm_play_data_start(NULL, 0); |
| } |
| } |
| else |
| { |
| logf(" no change"); |
| } |
| |
| pcm_play_unlock(); |
| } |
| |
| void pcm_play_stop(void) |
| { |
| logf("pcm_play_stop"); |
| |
| pcm_play_lock(); |
| |
| if (pcm_playing) |
| { |
| logf(" pcm_play_dma_stop"); |
| pcm_play_dma_stop(); |
| pcm_play_stopped(); |
| } |
| else |
| { |
| logf(" not playing"); |
| } |
| |
| pcm_play_unlock(); |
| } |
| |
| /**/ |
| |
| /* set frequency next frequency used by the audio hardware - |
| * what pcm_apply_settings will set */ |
| void pcm_set_frequency(unsigned int samplerate) |
| { |
| logf("pcm_set_frequency"); |
| |
| int index; |
| |
| #ifdef CONFIG_SAMPR_TYPES |
| #ifdef HAVE_RECORDING |
| unsigned int type = samplerate & SAMPR_TYPE_MASK; |
| #endif |
| samplerate &= ~SAMPR_TYPE_MASK; |
| |
| #ifdef HAVE_RECORDING |
| #if SAMPR_TYPE_REC != 0 |
| /* For now, supported targets have direct conversion when configured with |
| * CONFIG_SAMPR_TYPES. |
| * Some hypothetical target with independent rates would need slightly |
| * different handling throughout this source. */ |
| if (type == SAMPR_TYPE_REC) |
| samplerate = pcm_sampr_type_rec_to_play(samplerate); |
| #endif |
| #endif /* HAVE_RECORDING */ |
| #endif /* CONFIG_SAMPR_TYPES */ |
| |
| index = round_value_to_list32(samplerate, hw_freq_sampr, |
| HW_NUM_FREQ, false); |
| |
| if (samplerate != hw_freq_sampr[index]) |
| index = HW_FREQ_DEFAULT; /* Invalid = default */ |
| |
| pcm_sampr = hw_freq_sampr[index]; |
| pcm_fsel = index; |
| } |
| |
| /* apply pcm settings to the hardware */ |
| void pcm_apply_settings(void) |
| { |
| logf("pcm_apply_settings"); |
| |
| pcm_wait_for_init(); |
| |
| if (pcm_sampr != pcm_curr_sampr) |
| { |
| logf(" pcm_dma_apply_settings"); |
| pcm_dma_apply_settings(); |
| pcm_curr_sampr = pcm_sampr; |
| } |
| } |
| |
| /* register callback to buffer more data */ |
| void pcm_play_set_dma_started_callback(void (* callback)(void)) |
| { |
| pcm_play_dma_started = callback; |
| } |
| |
| #ifdef HAVE_RECORDING |
| /** Low level pcm recording apis **/ |
| |
| /* Next start for recording peaks */ |
| static const void * volatile pcm_rec_peak_addr SHAREDBSS_ATTR = NULL; |
| /* the registered callback function for when more data is available */ |
| static volatile pcm_rec_callback_type |
| pcm_callback_more_ready SHAREDBSS_ATTR = NULL; |
| /* DMA transfer in is currently active */ |
| volatile bool pcm_recording SHAREDBSS_ATTR = false; |
| |
| /* Called internally by functions to reset the state */ |
| static void pcm_recording_stopped(void) |
| { |
| pcm_recording = false; |
| pcm_callback_more_ready = NULL; |
| } |
| |
| /** |
| * Return recording peaks - From the end of the last peak up to |
| * current write position. |
| */ |
| void pcm_calculate_rec_peaks(int *left, int *right) |
| { |
| static uint16_t peaks[2]; |
| |
| if (pcm_recording) |
| { |
| const void *peak_addr = pcm_rec_peak_addr; |
| const void *addr = pcm_rec_dma_get_peak_buffer(); |
| |
| if (addr != NULL) |
| { |
| int count = (int)(((intptr_t)addr - (intptr_t)peak_addr) >> 2); |
| |
| if (count > 0) |
| { |
| pcm_peak_peeker((int32_t *)peak_addr, count, peaks); |
| |
| if (peak_addr == pcm_rec_peak_addr) |
| pcm_rec_peak_addr = addr; |
| } |
| } |
| /* else keep previous peak values */ |
| } |
| else |
| { |
| peaks[0] = peaks[1] = 0; |
| } |
| |
| if (left) |
| *left = peaks[0]; |
| |
| if (right) |
| *right = peaks[1]; |
| } /* pcm_calculate_rec_peaks */ |
| |
| bool pcm_is_recording(void) |
| { |
| return pcm_recording; |
| } |
| |
| /**************************************************************************** |
| * Functions that do not require targeted implementation but only a targeted |
| * interface |
| */ |
| |
| void pcm_init_recording(void) |
| { |
| logf("pcm_init_recording"); |
| |
| pcm_wait_for_init(); |
| |
| /* Stop the beasty before attempting recording */ |
| mixer_reset(); |
| |
| /* Recording init is locked unlike general pcm init since this is not |
| * just a one-time event at startup and it should and must be safe by |
| * now. */ |
| pcm_rec_lock(); |
| |
| logf(" pcm_rec_dma_init"); |
| pcm_recording_stopped(); |
| pcm_rec_dma_init(); |
| |
| pcm_rec_unlock(); |
| } |
| |
| void pcm_close_recording(void) |
| { |
| logf("pcm_close_recording"); |
| |
| pcm_rec_lock(); |
| |
| if (pcm_recording) |
| { |
| logf(" pcm_rec_dma_stop"); |
| pcm_rec_dma_stop(); |
| pcm_recording_stopped(); |
| } |
| |
| logf(" pcm_rec_dma_close"); |
| pcm_rec_dma_close(); |
| |
| pcm_rec_unlock(); |
| } |
| |
| void pcm_record_data(pcm_rec_callback_type more_ready, |
| void *start, size_t size) |
| { |
| logf("pcm_record_data"); |
| |
| ALIGN_AUDIOBUF(start, size); |
| |
| if (!(start && size)) |
| { |
| logf(" no buffer"); |
| return; |
| } |
| |
| pcm_rec_lock(); |
| |
| pcm_callback_more_ready = more_ready; |
| |
| #ifdef HAVE_PCM_REC_DMA_ADDRESS |
| /* Need a physical DMA address translation, if not already physical. */ |
| pcm_rec_peak_addr = pcm_dma_addr(start); |
| #else |
| pcm_rec_peak_addr = start; |
| #endif |
| |
| logf(" pcm_rec_dma_start"); |
| pcm_apply_settings(); |
| pcm_rec_dma_start(start, size); |
| pcm_recording = true; |
| |
| pcm_rec_unlock(); |
| } /* pcm_record_data */ |
| |
| void pcm_stop_recording(void) |
| { |
| logf("pcm_stop_recording"); |
| |
| pcm_rec_lock(); |
| |
| if (pcm_recording) |
| { |
| logf(" pcm_rec_dma_stop"); |
| pcm_rec_dma_stop(); |
| pcm_recording_stopped(); |
| } |
| |
| pcm_rec_unlock(); |
| } /* pcm_stop_recording */ |
| |
| void pcm_rec_more_ready_callback(int status, void **start, size_t *size) |
| { |
| pcm_rec_callback_type have_more = pcm_callback_more_ready; |
| |
| *size = 0; |
| |
| if (have_more && start) |
| { |
| have_more(status, start, size); |
| ALIGN_AUDIOBUF(*start, *size); |
| |
| if (*start && *size) |
| { |
| #ifdef HAVE_PCM_REC_DMA_ADDRESS |
| /* Need a physical DMA address translation, if not already |
| * physical. */ |
| pcm_rec_peak_addr = pcm_dma_addr(*start); |
| #else |
| pcm_rec_peak_addr = *start; |
| #endif |
| return; |
| } |
| } |
| |
| /* Error, callback missing or no more DMA to do */ |
| pcm_rec_dma_stop(); |
| pcm_recording_stopped(); |
| } |
| |
| #endif /* HAVE_RECORDING */ |