diff options
Diffstat (limited to 'hw/audio/lm4549.c')
-rw-r--r-- | hw/audio/lm4549.c | 336 |
1 files changed, 336 insertions, 0 deletions
diff --git a/hw/audio/lm4549.c b/hw/audio/lm4549.c new file mode 100644 index 0000000000..67335cba61 --- /dev/null +++ b/hw/audio/lm4549.c @@ -0,0 +1,336 @@ +/* + * LM4549 Audio Codec Interface + * + * Copyright (c) 2011 + * Written by Mathieu Sonet - www.elasticsheep.com + * + * This code is licensed under the GPL. + * + * ***************************************************************** + * + * This driver emulates the LM4549 codec. + * + * It supports only one playback voice and no record voice. + */ + +#include "hw/hw.h" +#include "audio/audio.h" +#include "hw/lm4549.h" + +#if 0 +#define LM4549_DEBUG 1 +#endif + +#if 0 +#define LM4549_DUMP_DAC_INPUT 1 +#endif + +#ifdef LM4549_DEBUG +#define DPRINTF(fmt, ...) \ +do { printf("lm4549: " fmt , ## __VA_ARGS__); } while (0) +#else +#define DPRINTF(fmt, ...) do {} while (0) +#endif + +#if defined(LM4549_DUMP_DAC_INPUT) +#include <stdio.h> +static FILE *fp_dac_input; +#endif + +/* LM4549 register list */ +enum { + LM4549_Reset = 0x00, + LM4549_Master_Volume = 0x02, + LM4549_Line_Out_Volume = 0x04, + LM4549_Master_Volume_Mono = 0x06, + LM4549_PC_Beep_Volume = 0x0A, + LM4549_Phone_Volume = 0x0C, + LM4549_Mic_Volume = 0x0E, + LM4549_Line_In_Volume = 0x10, + LM4549_CD_Volume = 0x12, + LM4549_Video_Volume = 0x14, + LM4549_Aux_Volume = 0x16, + LM4549_PCM_Out_Volume = 0x18, + LM4549_Record_Select = 0x1A, + LM4549_Record_Gain = 0x1C, + LM4549_General_Purpose = 0x20, + LM4549_3D_Control = 0x22, + LM4549_Powerdown_Ctrl_Stat = 0x26, + LM4549_Ext_Audio_ID = 0x28, + LM4549_Ext_Audio_Stat_Ctrl = 0x2A, + LM4549_PCM_Front_DAC_Rate = 0x2C, + LM4549_PCM_ADC_Rate = 0x32, + LM4549_Vendor_ID1 = 0x7C, + LM4549_Vendor_ID2 = 0x7E +}; + +static void lm4549_reset(lm4549_state *s) +{ + uint16_t *regfile = s->regfile; + + regfile[LM4549_Reset] = 0x0d50; + regfile[LM4549_Master_Volume] = 0x8008; + regfile[LM4549_Line_Out_Volume] = 0x8000; + regfile[LM4549_Master_Volume_Mono] = 0x8000; + regfile[LM4549_PC_Beep_Volume] = 0x0000; + regfile[LM4549_Phone_Volume] = 0x8008; + regfile[LM4549_Mic_Volume] = 0x8008; + regfile[LM4549_Line_In_Volume] = 0x8808; + regfile[LM4549_CD_Volume] = 0x8808; + regfile[LM4549_Video_Volume] = 0x8808; + regfile[LM4549_Aux_Volume] = 0x8808; + regfile[LM4549_PCM_Out_Volume] = 0x8808; + regfile[LM4549_Record_Select] = 0x0000; + regfile[LM4549_Record_Gain] = 0x8000; + regfile[LM4549_General_Purpose] = 0x0000; + regfile[LM4549_3D_Control] = 0x0101; + regfile[LM4549_Powerdown_Ctrl_Stat] = 0x000f; + regfile[LM4549_Ext_Audio_ID] = 0x0001; + regfile[LM4549_Ext_Audio_Stat_Ctrl] = 0x0000; + regfile[LM4549_PCM_Front_DAC_Rate] = 0xbb80; + regfile[LM4549_PCM_ADC_Rate] = 0xbb80; + regfile[LM4549_Vendor_ID1] = 0x4e53; + regfile[LM4549_Vendor_ID2] = 0x4331; +} + +static void lm4549_audio_transfer(lm4549_state *s) +{ + uint32_t written_bytes, written_samples; + uint32_t i; + + /* Activate the voice */ + AUD_set_active_out(s->voice, 1); + s->voice_is_active = 1; + + /* Try to write the buffer content */ + written_bytes = AUD_write(s->voice, s->buffer, + s->buffer_level * sizeof(uint16_t)); + written_samples = written_bytes >> 1; + +#if defined(LM4549_DUMP_DAC_INPUT) + fwrite(s->buffer, sizeof(uint8_t), written_bytes, fp_dac_input); +#endif + + s->buffer_level -= written_samples; + + if (s->buffer_level > 0) { + /* Move the data back to the start of the buffer */ + for (i = 0; i < s->buffer_level; i++) { + s->buffer[i] = s->buffer[i + written_samples]; + } + } +} + +static void lm4549_audio_out_callback(void *opaque, int free) +{ + lm4549_state *s = (lm4549_state *)opaque; + static uint32_t prev_buffer_level; + +#ifdef LM4549_DEBUG + int size = AUD_get_buffer_size_out(s->voice); + DPRINTF("audio_out_callback size = %i free = %i\n", size, free); +#endif + + /* Detect that no data are consumed + => disable the voice */ + if (s->buffer_level == prev_buffer_level) { + AUD_set_active_out(s->voice, 0); + s->voice_is_active = 0; + } + prev_buffer_level = s->buffer_level; + + /* Check if a buffer transfer is pending */ + if (s->buffer_level == LM4549_BUFFER_SIZE) { + lm4549_audio_transfer(s); + + /* Request more data */ + if (s->data_req_cb != NULL) { + (s->data_req_cb)(s->opaque); + } + } +} + +uint32_t lm4549_read(lm4549_state *s, hwaddr offset) +{ + uint16_t *regfile = s->regfile; + uint32_t value = 0; + + /* Read the stored value */ + assert(offset < 128); + value = regfile[offset]; + + DPRINTF("read [0x%02x] = 0x%04x\n", offset, value); + + return value; +} + +void lm4549_write(lm4549_state *s, + hwaddr offset, uint32_t value) +{ + uint16_t *regfile = s->regfile; + + assert(offset < 128); + DPRINTF("write [0x%02x] = 0x%04x\n", offset, value); + + switch (offset) { + case LM4549_Reset: + lm4549_reset(s); + break; + + case LM4549_PCM_Front_DAC_Rate: + regfile[LM4549_PCM_Front_DAC_Rate] = value; + DPRINTF("DAC rate change = %i\n", value); + + /* Re-open a voice with the new sample rate */ + struct audsettings as; + as.freq = value; + as.nchannels = 2; + as.fmt = AUD_FMT_S16; + as.endianness = 0; + + s->voice = AUD_open_out( + &s->card, + s->voice, + "lm4549.out", + s, + lm4549_audio_out_callback, + &as + ); + break; + + case LM4549_Powerdown_Ctrl_Stat: + value &= ~0xf; + value |= regfile[LM4549_Powerdown_Ctrl_Stat] & 0xf; + regfile[LM4549_Powerdown_Ctrl_Stat] = value; + break; + + case LM4549_Ext_Audio_ID: + case LM4549_Vendor_ID1: + case LM4549_Vendor_ID2: + DPRINTF("Write to read-only register 0x%x\n", (int)offset); + break; + + default: + /* Store the new value */ + regfile[offset] = value; + break; + } +} + +uint32_t lm4549_write_samples(lm4549_state *s, uint32_t left, uint32_t right) +{ + /* The left and right samples are in 20-bit resolution. + The LM4549 has 18-bit resolution and only uses the bits [19:2]. + This model supports 16-bit playback. + */ + + if (s->buffer_level > LM4549_BUFFER_SIZE - 2) { + DPRINTF("write_sample Buffer full\n"); + return 0; + } + + /* Store 16-bit samples in the buffer */ + s->buffer[s->buffer_level++] = (left >> 4); + s->buffer[s->buffer_level++] = (right >> 4); + + if (s->buffer_level == LM4549_BUFFER_SIZE) { + /* Trigger the transfer of the buffer to the audio host */ + lm4549_audio_transfer(s); + } + + return 1; +} + +static int lm4549_post_load(void *opaque, int version_id) +{ + lm4549_state *s = (lm4549_state *)opaque; + uint16_t *regfile = s->regfile; + + /* Re-open a voice with the current sample rate */ + uint32_t freq = regfile[LM4549_PCM_Front_DAC_Rate]; + + DPRINTF("post_load freq = %i\n", freq); + DPRINTF("post_load voice_is_active = %i\n", s->voice_is_active); + + struct audsettings as; + as.freq = freq; + as.nchannels = 2; + as.fmt = AUD_FMT_S16; + as.endianness = 0; + + s->voice = AUD_open_out( + &s->card, + s->voice, + "lm4549.out", + s, + lm4549_audio_out_callback, + &as + ); + + /* Request data */ + if (s->voice_is_active == 1) { + lm4549_audio_out_callback(s, AUD_get_buffer_size_out(s->voice)); + } + + return 0; +} + +void lm4549_init(lm4549_state *s, lm4549_callback data_req_cb, void* opaque) +{ + struct audsettings as; + + /* Store the callback and opaque pointer */ + s->data_req_cb = data_req_cb; + s->opaque = opaque; + + /* Init the registers */ + lm4549_reset(s); + + /* Register an audio card */ + AUD_register_card("lm4549", &s->card); + + /* Open a default voice */ + as.freq = 48000; + as.nchannels = 2; + as.fmt = AUD_FMT_S16; + as.endianness = 0; + + s->voice = AUD_open_out( + &s->card, + s->voice, + "lm4549.out", + s, + lm4549_audio_out_callback, + &as + ); + + AUD_set_volume_out(s->voice, 0, 255, 255); + + s->voice_is_active = 0; + + /* Reset the input buffer */ + memset(s->buffer, 0x00, sizeof(s->buffer)); + s->buffer_level = 0; + +#if defined(LM4549_DUMP_DAC_INPUT) + fp_dac_input = fopen("lm4549_dac_input.pcm", "wb"); + if (!fp_dac_input) { + hw_error("Unable to open lm4549_dac_input.pcm for writing\n"); + } +#endif +} + +const VMStateDescription vmstate_lm4549_state = { + .name = "lm4549_state", + .version_id = 1, + .minimum_version_id = 1, + .minimum_version_id_old = 1, + .post_load = &lm4549_post_load, + .fields = (VMStateField[]) { + VMSTATE_UINT32(voice_is_active, lm4549_state), + VMSTATE_UINT16_ARRAY(regfile, lm4549_state, 128), + VMSTATE_UINT16_ARRAY(buffer, lm4549_state, LM4549_BUFFER_SIZE), + VMSTATE_UINT32(buffer_level, lm4549_state), + VMSTATE_END_OF_LIST() + } +}; |