| /* |
| * QEMU Apple Sound Chip emulation |
| * |
| * Apple Sound Chip (ASC) 344S0063 |
| * Enhanced Apple Sound Chip (EASC) 343S1063 |
| * |
| * Copyright (c) 2012-2018 Laurent Vivier <laurent@vivier.eu> |
| * Copyright (c) 2022 Mark Cave-Ayland <mark.cave-ayland@ilande.co.uk> |
| * |
| * SPDX-License-Identifier: GPL-2.0-or-later |
| */ |
| |
| #include "qemu/osdep.h" |
| #include "qemu/timer.h" |
| #include "hw/sysbus.h" |
| #include "hw/irq.h" |
| #include "audio/audio.h" |
| #include "hw/audio/asc.h" |
| #include "hw/qdev-properties.h" |
| #include "migration/vmstate.h" |
| #include "trace.h" |
| |
| /* |
| * Linux doesn't provide information about ASC, see arch/m68k/mac/macboing.c |
| * and arch/m68k/include/asm/mac_asc.h |
| * |
| * best information is coming from MAME: |
| * https://github.com/mamedev/mame/blob/master/src/devices/sound/asc.h |
| * https://github.com/mamedev/mame/blob/master/src/devices/sound/asc.cpp |
| * Emulation by R. Belmont |
| * or MESS: |
| * http://mess.redump.net/mess/driver_info/easc |
| * |
| * 0x800: VERSION |
| * 0x801: MODE |
| * 1=FIFO mode, |
| * 2=wavetable mode |
| * 0x802: CONTROL |
| * bit 0=analog or PWM output, |
| * 1=stereo/mono, |
| * 7=processing time exceeded |
| * 0x803: FIFO MODE |
| * bit 7=clear FIFO, |
| * bit 1="non-ROM companding", |
| * bit 0="ROM companding") |
| * 0x804: FIFO IRQ STATUS |
| * bit 0=ch A 1/2 full, |
| * 1=ch A full, |
| * 2=ch B 1/2 full, |
| * 3=ch B full) |
| * 0x805: WAVETABLE CONTROL |
| * bits 0-3 wavetables 0-3 start |
| * 0x806: VOLUME |
| * bits 2-4 = 3 bit internal ASC volume, |
| * bits 5-7 = volume control sent to Sony sound chip |
| * 0x807: CLOCK RATE |
| * 0 = Mac 22257 Hz, |
| * 1 = undefined, |
| * 2 = 22050 Hz, |
| * 3 = 44100 Hz |
| * 0x80a: PLAY REC A |
| * 0x80f: TEST |
| * bits 6-7 = digital test, |
| * bits 4-5 = analog test |
| * 0x810: WAVETABLE 0 PHASE |
| * big-endian 9.15 fixed-point, only 24 bits valid |
| * 0x814: WAVETABLE 0 INCREMENT |
| * big-endian 9.15 fixed-point, only 24 bits valid |
| * 0x818: WAVETABLE 1 PHASE |
| * 0x81C: WAVETABLE 1 INCREMENT |
| * 0x820: WAVETABLE 2 PHASE |
| * 0x824: WAVETABLE 2 INCREMENT |
| * 0x828: WAVETABLE 3 PHASE |
| * 0x82C: WAVETABLE 3 INCREMENT |
| * 0x830: UNKNOWN START |
| * NetBSD writes Wavetable data here (are there more |
| * wavetables/channels than we know about?) |
| * 0x857: UNKNOWN END |
| */ |
| |
| #define ASC_SIZE 0x2000 |
| |
| enum { |
| ASC_VERSION = 0x00, |
| ASC_MODE = 0x01, |
| ASC_CONTROL = 0x02, |
| ASC_FIFOMODE = 0x03, |
| ASC_FIFOIRQ = 0x04, |
| ASC_WAVECTRL = 0x05, |
| ASC_VOLUME = 0x06, |
| ASC_CLOCK = 0x07, |
| ASC_PLAYRECA = 0x0a, |
| ASC_TEST = 0x0f, |
| ASC_WAVETABLE = 0x10 |
| }; |
| |
| #define ASC_FIFO_STATUS_HALF_FULL 1 |
| #define ASC_FIFO_STATUS_FULL_EMPTY 2 |
| |
| #define ASC_EXTREGS_FIFOCTRL 0x8 |
| #define ASC_EXTREGS_INTCTRL 0x9 |
| #define ASC_EXTREGS_CDXA_DECOMP_FILT 0x10 |
| |
| #define ASC_FIFO_CYCLE_TIME ((NANOSECONDS_PER_SECOND / ASC_FREQ) * \ |
| 0x400) |
| |
| static void asc_raise_irq(ASCState *s) |
| { |
| qemu_set_irq(s->irq, 1); |
| } |
| |
| static void asc_lower_irq(ASCState *s) |
| { |
| qemu_set_irq(s->irq, 0); |
| } |
| |
| static uint8_t asc_fifo_get(ASCFIFOState *fs) |
| { |
| ASCState *s = container_of(fs, ASCState, fifos[fs->index]); |
| bool fifo_half_irq_enabled = fs->extregs[ASC_EXTREGS_INTCTRL] & 1; |
| uint8_t val; |
| |
| assert(fs->cnt); |
| |
| val = fs->fifo[fs->rptr]; |
| trace_asc_fifo_get('A' + fs->index, fs->rptr, fs->cnt, val); |
| |
| fs->rptr++; |
| fs->rptr &= 0x3ff; |
| fs->cnt--; |
| |
| if (fs->cnt <= 0x1ff) { |
| /* FIFO less than half full */ |
| fs->int_status |= ASC_FIFO_STATUS_HALF_FULL; |
| } else { |
| /* FIFO more than half full */ |
| fs->int_status &= ~ASC_FIFO_STATUS_HALF_FULL; |
| } |
| |
| if (fs->cnt == 0x1ff && fifo_half_irq_enabled) { |
| /* Raise FIFO half full IRQ */ |
| asc_raise_irq(s); |
| } |
| |
| if (fs->cnt == 0) { |
| /* Raise FIFO empty IRQ */ |
| fs->int_status |= ASC_FIFO_STATUS_FULL_EMPTY; |
| asc_raise_irq(s); |
| } |
| |
| return val; |
| } |
| |
| static int generate_fifo(ASCState *s, int maxsamples) |
| { |
| int64_t now = qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL); |
| uint8_t *buf = s->mixbuf; |
| int i, wcount = 0; |
| |
| while (wcount < maxsamples) { |
| uint8_t val; |
| int16_t d, f0, f1; |
| int32_t t; |
| int shift, filter; |
| bool hasdata = false; |
| |
| for (i = 0; i < 2; i++) { |
| ASCFIFOState *fs = &s->fifos[i]; |
| |
| switch (fs->extregs[ASC_EXTREGS_FIFOCTRL] & 0x83) { |
| case 0x82: |
| /* |
| * CD-XA BRR mode: decompress 15 bytes into 28 16-bit |
| * samples |
| */ |
| if (!fs->cnt) { |
| val = 0x80; |
| break; |
| } |
| |
| if (fs->xa_cnt == -1) { |
| /* Start of packet, get flags */ |
| fs->xa_flags = asc_fifo_get(fs); |
| fs->xa_cnt = 0; |
| } |
| |
| shift = fs->xa_flags & 0xf; |
| filter = fs->xa_flags >> 4; |
| f0 = (int8_t)fs->extregs[ASC_EXTREGS_CDXA_DECOMP_FILT + |
| (filter << 1) + 1]; |
| f1 = (int8_t)fs->extregs[ASC_EXTREGS_CDXA_DECOMP_FILT + |
| (filter << 1)]; |
| |
| if ((fs->xa_cnt & 1) == 0) { |
| if (!fs->cnt) { |
| val = 0x80; |
| break; |
| } |
| |
| fs->xa_val = asc_fifo_get(fs); |
| d = (fs->xa_val & 0xf) << 12; |
| } else { |
| d = (fs->xa_val & 0xf0) << 8; |
| } |
| t = (d >> shift) + (((fs->xa_last[0] * f0) + |
| (fs->xa_last[1] * f1) + 32) >> 6); |
| if (t < -32768) { |
| t = -32768; |
| } else if (t > 32767) { |
| t = 32767; |
| } |
| |
| /* |
| * CD-XA BRR generates 16-bit signed output, so convert to |
| * 8-bit before writing to buffer. Does real hardware do the |
| * same? |
| */ |
| val = (uint8_t)(t / 256) ^ 0x80; |
| hasdata = true; |
| fs->xa_cnt++; |
| |
| fs->xa_last[1] = fs->xa_last[0]; |
| fs->xa_last[0] = (int16_t)t; |
| |
| if (fs->xa_cnt == 28) { |
| /* End of packet */ |
| fs->xa_cnt = -1; |
| } |
| break; |
| |
| default: |
| /* fallthrough */ |
| case 0x80: |
| /* Raw mode */ |
| if (fs->cnt) { |
| val = asc_fifo_get(fs); |
| hasdata = true; |
| } else { |
| val = 0x80; |
| } |
| break; |
| } |
| |
| buf[wcount * 2 + i] = val; |
| } |
| |
| if (!hasdata) { |
| break; |
| } |
| |
| wcount++; |
| } |
| |
| /* |
| * MacOS (un)helpfully leaves the FIFO engine running even when it has |
| * finished writing out samples, but still expects the FIFO empty |
| * interrupts to be generated for each FIFO cycle (without these interrupts |
| * MacOS will freeze) |
| */ |
| if (s->fifos[0].cnt == 0 && s->fifos[1].cnt == 0) { |
| if (!s->fifo_empty_ns) { |
| /* FIFO has completed first empty cycle */ |
| s->fifo_empty_ns = now; |
| } else if (now > (s->fifo_empty_ns + ASC_FIFO_CYCLE_TIME)) { |
| /* FIFO has completed entire cycle with no data */ |
| s->fifos[0].int_status |= ASC_FIFO_STATUS_HALF_FULL | |
| ASC_FIFO_STATUS_FULL_EMPTY; |
| s->fifos[1].int_status |= ASC_FIFO_STATUS_HALF_FULL | |
| ASC_FIFO_STATUS_FULL_EMPTY; |
| s->fifo_empty_ns = now; |
| asc_raise_irq(s); |
| } |
| } else { |
| /* FIFO contains data, reset empty time */ |
| s->fifo_empty_ns = 0; |
| } |
| |
| return wcount; |
| } |
| |
| static int generate_wavetable(ASCState *s, int maxsamples) |
| { |
| uint8_t *buf = s->mixbuf; |
| int channel, count = 0; |
| |
| while (count < maxsamples) { |
| uint32_t left = 0, right = 0; |
| uint8_t sample; |
| |
| for (channel = 0; channel < 4; channel++) { |
| ASCFIFOState *fs = &s->fifos[channel >> 1]; |
| int chanreg = ASC_WAVETABLE + (channel << 3); |
| uint32_t phase, incr, offset; |
| |
| phase = ldl_be_p(&s->regs[chanreg]); |
| incr = ldl_be_p(&s->regs[chanreg + sizeof(uint32_t)]); |
| |
| phase += incr; |
| offset = (phase >> 15) & 0x1ff; |
| sample = fs->fifo[0x200 * (channel >> 1) + offset]; |
| |
| stl_be_p(&s->regs[chanreg], phase); |
| |
| left += sample; |
| right += sample; |
| } |
| |
| buf[count * 2] = left >> 2; |
| buf[count * 2 + 1] = right >> 2; |
| |
| count++; |
| } |
| |
| return count; |
| } |
| |
| static void asc_out_cb(void *opaque, int free_b) |
| { |
| ASCState *s = opaque; |
| int samples, generated; |
| |
| if (free_b == 0) { |
| return; |
| } |
| |
| samples = MIN(s->samples, free_b >> s->shift); |
| |
| switch (s->regs[ASC_MODE] & 3) { |
| default: |
| /* Off */ |
| generated = 0; |
| break; |
| case 1: |
| /* FIFO mode */ |
| generated = generate_fifo(s, samples); |
| break; |
| case 2: |
| /* Wave table mode */ |
| generated = generate_wavetable(s, samples); |
| break; |
| } |
| |
| if (!generated) { |
| /* Workaround for audio underflow bug on Windows dsound backend */ |
| int64_t now = qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL); |
| int silent_samples = muldiv64(now - s->fifo_empty_ns, |
| NANOSECONDS_PER_SECOND, ASC_FREQ); |
| |
| if (silent_samples > ASC_FIFO_CYCLE_TIME / 2) { |
| /* |
| * No new FIFO data within half a cycle time (~23ms) so fill the |
| * entire available buffer with silence. This prevents an issue |
| * with the Windows dsound backend whereby the sound appears to |
| * loop because the FIFO has run out of data, and the driver |
| * reuses the stale content in its circular audio buffer. |
| */ |
| AUD_write(s->voice, s->silentbuf, samples << s->shift); |
| } |
| return; |
| } |
| |
| AUD_write(s->voice, s->mixbuf, generated << s->shift); |
| } |
| |
| static uint64_t asc_fifo_read(void *opaque, hwaddr addr, |
| unsigned size) |
| { |
| ASCFIFOState *fs = opaque; |
| |
| trace_asc_read_fifo('A' + fs->index, addr, size, fs->fifo[addr]); |
| return fs->fifo[addr]; |
| } |
| |
| static void asc_fifo_write(void *opaque, hwaddr addr, uint64_t value, |
| unsigned size) |
| { |
| ASCFIFOState *fs = opaque; |
| ASCState *s = container_of(fs, ASCState, fifos[fs->index]); |
| bool fifo_half_irq_enabled = fs->extregs[ASC_EXTREGS_INTCTRL] & 1; |
| |
| trace_asc_write_fifo('A' + fs->index, addr, size, fs->wptr, fs->cnt, value); |
| |
| if (s->regs[ASC_MODE] == 1) { |
| fs->fifo[fs->wptr++] = value; |
| fs->wptr &= 0x3ff; |
| fs->cnt++; |
| |
| if (fs->cnt <= 0x1ff) { |
| /* FIFO less than half full */ |
| fs->int_status |= ASC_FIFO_STATUS_HALF_FULL; |
| } else { |
| /* FIFO at least half full */ |
| fs->int_status &= ~ASC_FIFO_STATUS_HALF_FULL; |
| } |
| |
| if (fs->cnt == 0x200 && fifo_half_irq_enabled) { |
| /* Raise FIFO half full interrupt */ |
| asc_raise_irq(s); |
| } |
| |
| if (fs->cnt == 0x3ff) { |
| /* Raise FIFO full interrupt */ |
| fs->int_status |= ASC_FIFO_STATUS_FULL_EMPTY; |
| asc_raise_irq(s); |
| } |
| } else { |
| fs->fifo[addr] = value; |
| } |
| return; |
| } |
| |
| static const MemoryRegionOps asc_fifo_ops = { |
| .read = asc_fifo_read, |
| .write = asc_fifo_write, |
| .impl = { |
| .min_access_size = 1, |
| .max_access_size = 1, |
| }, |
| .endianness = DEVICE_BIG_ENDIAN, |
| }; |
| |
| static void asc_fifo_reset(ASCFIFOState *fs); |
| |
| static uint64_t asc_read(void *opaque, hwaddr addr, |
| unsigned size) |
| { |
| ASCState *s = opaque; |
| uint64_t prev, value; |
| |
| switch (addr) { |
| case ASC_VERSION: |
| switch (s->type) { |
| default: |
| case ASC_TYPE_ASC: |
| value = 0; |
| break; |
| case ASC_TYPE_EASC: |
| value = 0xb0; |
| break; |
| } |
| break; |
| case ASC_FIFOIRQ: |
| prev = (s->fifos[0].int_status & 0x3) | |
| (s->fifos[1].int_status & 0x3) << 2; |
| |
| s->fifos[0].int_status = 0; |
| s->fifos[1].int_status = 0; |
| asc_lower_irq(s); |
| value = prev; |
| break; |
| default: |
| value = s->regs[addr]; |
| break; |
| } |
| |
| trace_asc_read_reg(addr, size, value); |
| return value; |
| } |
| |
| static void asc_write(void *opaque, hwaddr addr, uint64_t value, |
| unsigned size) |
| { |
| ASCState *s = opaque; |
| |
| switch (addr) { |
| case ASC_MODE: |
| value &= 3; |
| if (value != s->regs[ASC_MODE]) { |
| asc_fifo_reset(&s->fifos[0]); |
| asc_fifo_reset(&s->fifos[1]); |
| asc_lower_irq(s); |
| if (value != 0) { |
| AUD_set_active_out(s->voice, 1); |
| } else { |
| AUD_set_active_out(s->voice, 0); |
| } |
| } |
| break; |
| case ASC_FIFOMODE: |
| if (value & 0x80) { |
| asc_fifo_reset(&s->fifos[0]); |
| asc_fifo_reset(&s->fifos[1]); |
| asc_lower_irq(s); |
| } |
| break; |
| case ASC_WAVECTRL: |
| break; |
| case ASC_VOLUME: |
| { |
| int vol = (value & 0xe0); |
| |
| AUD_set_volume_out(s->voice, 0, vol, vol); |
| break; |
| } |
| } |
| |
| trace_asc_write_reg(addr, size, value); |
| s->regs[addr] = value; |
| } |
| |
| static const MemoryRegionOps asc_regs_ops = { |
| .read = asc_read, |
| .write = asc_write, |
| .endianness = DEVICE_BIG_ENDIAN, |
| .impl = { |
| .min_access_size = 1, |
| .max_access_size = 1, |
| } |
| }; |
| |
| static uint64_t asc_ext_read(void *opaque, hwaddr addr, |
| unsigned size) |
| { |
| ASCFIFOState *fs = opaque; |
| uint64_t value; |
| |
| value = fs->extregs[addr]; |
| |
| trace_asc_read_extreg('A' + fs->index, addr, size, value); |
| return value; |
| } |
| |
| static void asc_ext_write(void *opaque, hwaddr addr, uint64_t value, |
| unsigned size) |
| { |
| ASCFIFOState *fs = opaque; |
| |
| trace_asc_write_extreg('A' + fs->index, addr, size, value); |
| |
| fs->extregs[addr] = value; |
| } |
| |
| static const MemoryRegionOps asc_extregs_ops = { |
| .read = asc_ext_read, |
| .write = asc_ext_write, |
| .impl = { |
| .min_access_size = 1, |
| .max_access_size = 1, |
| }, |
| .endianness = DEVICE_BIG_ENDIAN, |
| }; |
| |
| static int asc_post_load(void *opaque, int version) |
| { |
| ASCState *s = ASC(opaque); |
| |
| if (s->regs[ASC_MODE] != 0) { |
| AUD_set_active_out(s->voice, 1); |
| } |
| |
| return 0; |
| } |
| |
| static const VMStateDescription vmstate_asc_fifo = { |
| .name = "apple-sound-chip.fifo", |
| .version_id = 0, |
| .minimum_version_id = 0, |
| .fields = (const VMStateField[]) { |
| VMSTATE_UINT8_ARRAY(fifo, ASCFIFOState, ASC_FIFO_SIZE), |
| VMSTATE_UINT8(int_status, ASCFIFOState), |
| VMSTATE_INT32(cnt, ASCFIFOState), |
| VMSTATE_INT32(wptr, ASCFIFOState), |
| VMSTATE_INT32(rptr, ASCFIFOState), |
| VMSTATE_UINT8_ARRAY(extregs, ASCFIFOState, ASC_EXTREG_SIZE), |
| VMSTATE_INT32(xa_cnt, ASCFIFOState), |
| VMSTATE_UINT8(xa_val, ASCFIFOState), |
| VMSTATE_UINT8(xa_flags, ASCFIFOState), |
| VMSTATE_INT16_ARRAY(xa_last, ASCFIFOState, 2), |
| VMSTATE_END_OF_LIST() |
| } |
| }; |
| |
| static const VMStateDescription vmstate_asc = { |
| .name = "apple-sound-chip", |
| .version_id = 0, |
| .minimum_version_id = 0, |
| .post_load = asc_post_load, |
| .fields = (const VMStateField[]) { |
| VMSTATE_STRUCT_ARRAY(fifos, ASCState, 2, 0, vmstate_asc_fifo, |
| ASCFIFOState), |
| VMSTATE_UINT8_ARRAY(regs, ASCState, ASC_REG_SIZE), |
| VMSTATE_INT64(fifo_empty_ns, ASCState), |
| VMSTATE_END_OF_LIST() |
| } |
| }; |
| |
| static void asc_fifo_reset(ASCFIFOState *fs) |
| { |
| fs->wptr = 0; |
| fs->rptr = 0; |
| fs->cnt = 0; |
| fs->xa_cnt = -1; |
| fs->int_status = 0; |
| } |
| |
| static void asc_fifo_init(ASCFIFOState *fs, int index) |
| { |
| ASCState *s = container_of(fs, ASCState, fifos[index]); |
| char *name; |
| |
| fs->index = index; |
| name = g_strdup_printf("asc.fifo%c", 'A' + index); |
| memory_region_init_io(&fs->mem_fifo, OBJECT(s), &asc_fifo_ops, fs, |
| name, ASC_FIFO_SIZE); |
| g_free(name); |
| |
| name = g_strdup_printf("asc.extregs%c", 'A' + index); |
| memory_region_init_io(&fs->mem_extregs, OBJECT(s), &asc_extregs_ops, |
| fs, name, ASC_EXTREG_SIZE); |
| g_free(name); |
| } |
| |
| static void asc_reset_hold(Object *obj) |
| { |
| ASCState *s = ASC(obj); |
| |
| AUD_set_active_out(s->voice, 0); |
| |
| memset(s->regs, 0, sizeof(s->regs)); |
| asc_fifo_reset(&s->fifos[0]); |
| asc_fifo_reset(&s->fifos[1]); |
| s->fifo_empty_ns = 0; |
| |
| if (s->type == ASC_TYPE_ASC) { |
| /* FIFO half full IRQs enabled by default */ |
| s->fifos[0].extregs[ASC_EXTREGS_INTCTRL] = 1; |
| s->fifos[1].extregs[ASC_EXTREGS_INTCTRL] = 1; |
| } |
| } |
| |
| static void asc_unrealize(DeviceState *dev) |
| { |
| ASCState *s = ASC(dev); |
| |
| g_free(s->mixbuf); |
| g_free(s->silentbuf); |
| |
| AUD_remove_card(&s->card); |
| } |
| |
| static void asc_realize(DeviceState *dev, Error **errp) |
| { |
| ASCState *s = ASC(dev); |
| struct audsettings as; |
| |
| if (!AUD_register_card("Apple Sound Chip", &s->card, errp)) { |
| return; |
| } |
| |
| as.freq = ASC_FREQ; |
| as.nchannels = 2; |
| as.fmt = AUDIO_FORMAT_U8; |
| as.endianness = AUDIO_HOST_ENDIANNESS; |
| |
| s->voice = AUD_open_out(&s->card, s->voice, "asc.out", s, asc_out_cb, |
| &as); |
| s->shift = 1; |
| s->samples = AUD_get_buffer_size_out(s->voice) >> s->shift; |
| s->mixbuf = g_malloc0(s->samples << s->shift); |
| |
| s->silentbuf = g_malloc0(s->samples << s->shift); |
| memset(s->silentbuf, 0x80, s->samples << s->shift); |
| |
| /* Add easc registers if required */ |
| if (s->type == ASC_TYPE_EASC) { |
| memory_region_add_subregion(&s->asc, ASC_EXTREG_OFFSET, |
| &s->fifos[0].mem_extregs); |
| memory_region_add_subregion(&s->asc, |
| ASC_EXTREG_OFFSET + ASC_EXTREG_SIZE, |
| &s->fifos[1].mem_extregs); |
| } |
| } |
| |
| static void asc_init(Object *obj) |
| { |
| ASCState *s = ASC(obj); |
| SysBusDevice *sbd = SYS_BUS_DEVICE(obj); |
| |
| memory_region_init(&s->asc, OBJECT(obj), "asc", ASC_SIZE); |
| |
| asc_fifo_init(&s->fifos[0], 0); |
| asc_fifo_init(&s->fifos[1], 1); |
| |
| memory_region_add_subregion(&s->asc, ASC_FIFO_OFFSET, |
| &s->fifos[0].mem_fifo); |
| memory_region_add_subregion(&s->asc, |
| ASC_FIFO_OFFSET + ASC_FIFO_SIZE, |
| &s->fifos[1].mem_fifo); |
| |
| memory_region_init_io(&s->mem_regs, OBJECT(obj), &asc_regs_ops, s, |
| "asc.regs", ASC_REG_SIZE); |
| memory_region_add_subregion(&s->asc, ASC_REG_OFFSET, &s->mem_regs); |
| |
| sysbus_init_irq(sbd, &s->irq); |
| sysbus_init_mmio(sbd, &s->asc); |
| } |
| |
| static Property asc_properties[] = { |
| DEFINE_AUDIO_PROPERTIES(ASCState, card), |
| DEFINE_PROP_UINT8("asctype", ASCState, type, ASC_TYPE_ASC), |
| DEFINE_PROP_END_OF_LIST(), |
| }; |
| |
| static void asc_class_init(ObjectClass *oc, void *data) |
| { |
| DeviceClass *dc = DEVICE_CLASS(oc); |
| ResettableClass *rc = RESETTABLE_CLASS(oc); |
| |
| dc->realize = asc_realize; |
| dc->unrealize = asc_unrealize; |
| set_bit(DEVICE_CATEGORY_SOUND, dc->categories); |
| dc->vmsd = &vmstate_asc; |
| device_class_set_props(dc, asc_properties); |
| rc->phases.hold = asc_reset_hold; |
| } |
| |
| static const TypeInfo asc_info_types[] = { |
| { |
| .name = TYPE_ASC, |
| .parent = TYPE_SYS_BUS_DEVICE, |
| .instance_size = sizeof(ASCState), |
| .instance_init = asc_init, |
| .class_init = asc_class_init, |
| }, |
| }; |
| |
| DEFINE_TYPES(asc_info_types) |