diff mediaplayer.c @ 2289:92449b47cce8

Integrate VGM player into main blastem binary
author Michael Pavone <pavone@retrodev.com>
date Sat, 04 Feb 2023 22:44:44 -0800
parents
children c4980d89614b
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mediaplayer.c	Sat Feb 04 22:44:44 2023 -0800
@@ -0,0 +1,629 @@
+#include <stdlib.h>
+#include <stddef.h>
+#include <limits.h>
+#include <string.h>
+#include "mediaplayer.h"
+#include "io.h"
+#include "ym2612.h"
+#include "psg.h"
+#include "rf5c164.h"
+#include "util.h"
+#include "render.h"
+
+#define ADJUST_BUFFER (12500000)
+#define MAX_NO_ADJUST (UINT_MAX-ADJUST_BUFFER)
+#define MAX_RUN_SAMPLES 128
+
+enum {
+	AUDIO_VGM,
+	AUDIO_WAVE,
+	AUDIO_FLAC,
+	MEDIA_UNKNOWN
+};
+
+enum {
+	STATE_PLAY,
+	STATE_PAUSED
+};
+
+uint32_t cycles_to_samples(uint32_t clock_rate, uint32_t cycles)
+{
+	return ((uint64_t)cycles) * ((uint64_t)44100) / ((uint64_t)clock_rate);
+}
+
+uint32_t samples_to_cycles(uint32_t clock_rate, uint32_t cycles)
+{
+	return ((uint64_t)cycles) * ((uint64_t)clock_rate) / ((uint64_t)44100);
+}
+
+void ym_adjust(chip_info *chip)
+{
+	ym2612_context *ym = chip->context;
+	if (ym->current_cycle >= MAX_NO_ADJUST) {
+		uint32_t deduction = ym->current_cycle - ADJUST_BUFFER;
+		chip->samples -= cycles_to_samples(chip->clock, deduction);
+		ym->current_cycle -= deduction;
+	}
+}
+
+void psg_adjust(chip_info *chip)
+{
+	psg_context *psg = chip->context;
+	if (psg->cycles >= MAX_NO_ADJUST) {
+		uint32_t deduction = psg->cycles - ADJUST_BUFFER;
+		chip->samples -= cycles_to_samples(chip->clock, deduction);
+		psg->cycles -= deduction;
+	}
+}
+
+void pcm_adjust(chip_info *chip)
+{
+	rf5c164 *pcm = chip->context;
+	if (pcm->cycle >= MAX_NO_ADJUST) {
+		uint32_t deduction = pcm->cycle - ADJUST_BUFFER;
+		chip->samples -= cycles_to_samples(chip->clock, deduction);
+		pcm->cycle -= deduction;
+	}
+}
+
+uint8_t *find_block(data_block *head, uint32_t offset, uint32_t size)
+{
+	if (!head) {
+		return NULL;
+	}
+	while (head->size < offset) {
+		offset -= head->size;
+		head = head->next;
+	}
+	if (head->size - offset < size) {
+		return NULL;
+	}
+	return head->data + offset;
+}
+
+void vgm_wait(media_player *player, uint32_t samples)
+{
+	chip_info *chips = player->chips;
+	uint32_t num_chips = player->num_chips;
+	while (samples > MAX_RUN_SAMPLES)
+	{
+		vgm_wait(player, MAX_RUN_SAMPLES);
+		samples -= MAX_RUN_SAMPLES;
+	}
+	for (uint32_t i = 0; i < num_chips; i++)
+	{
+		chips[i].samples += samples;
+		chips[i].run(chips[i].context, samples_to_cycles(chips[i].clock, chips[i].samples));
+		chips[i].adjust(chips + i);
+	}
+}
+
+void vgm_stop(media_player *player)
+{
+	player->state = STATE_PAUSED;
+	player->playback_time = 0;
+	player->current_offset = player->vgm->data_offset + offsetof(vgm_header, data_offset);
+}
+
+chip_info *find_chip(media_player *player, uint8_t cmd)
+{
+	for (uint32_t i = 0; i < player->num_chips; i++)
+	{
+		if (player->chips[i].cmd == cmd) {
+			return player->chips + i;
+		}
+	}
+	return NULL;
+}
+
+void *find_chip_context(media_player *player, uint8_t cmd)
+{
+	chip_info *chip = find_chip(player, cmd);
+	return chip ? chip->context : NULL;
+}
+
+chip_info *find_chip_by_data(media_player *player, uint8_t data_type)
+{
+	for (uint32_t i = 0; i < player->num_chips; i++)
+	{
+		if (player->chips[i].data_type == data_type) {
+			return &player->chips[i];
+		}
+	}
+	return NULL;
+}
+
+static uint8_t read_byte(media_player *player)
+{
+	uint8_t *buffer = player->media->buffer;
+	return buffer[player->current_offset++];
+}
+
+static uint16_t read_word_le(media_player *player)
+{
+	uint8_t *buffer = player->media->buffer;
+	uint16_t value =  buffer[player->current_offset++];
+	value |= buffer[player->current_offset++] << 8;
+	return value;
+}
+
+static uint32_t read_24_le(media_player *player)
+{
+	uint8_t *buffer = player->media->buffer;
+	uint32_t value =  buffer[player->current_offset++];
+	value |= buffer[player->current_offset++] << 8;
+	value |= buffer[player->current_offset++] << 16;
+	return value;
+}
+
+static uint32_t read_long_le(media_player *player)
+{
+	uint8_t *buffer = player->media->buffer;
+	uint32_t value =  buffer[player->current_offset++];
+	value |= buffer[player->current_offset++] << 8;
+	value |= buffer[player->current_offset++] << 16;
+	value |= buffer[player->current_offset++] << 24;
+	return value;
+}
+
+void vgm_frame(media_player *player)
+{
+	for (uint32_t remaining_samples = 44100 / 60; remaining_samples > 0;)
+	{
+		if (player->wait_samples) {
+			uint32_t to_wait = player->wait_samples;
+			if (to_wait > remaining_samples) {
+				to_wait = remaining_samples;
+			}
+			vgm_wait(player, to_wait);
+			player->wait_samples -= to_wait;
+			remaining_samples -= to_wait;
+			if (player->wait_samples) {
+				return;
+			}
+		}
+		if (player->current_offset >= player->media->size) {
+			vgm_stop(player);
+			return;
+		}
+		uint8_t cmd = read_byte(player);
+		psg_context *psg;
+		ym2612_context *ym;
+		rf5c164 *pcm;
+		switch (cmd)
+		{
+		case CMD_PSG_STEREO:
+			psg = find_chip_context(player, CMD_PSG);
+			if (!psg || player->current_offset > player->media->size - 1) {
+				vgm_stop(player);
+				return;
+			}
+			psg->pan = read_byte(player);
+			break;
+		case CMD_PSG:
+			psg = find_chip_context(player, CMD_PSG);
+			if (!psg || player->current_offset > player->media->size - 1) {
+				vgm_stop(player);
+				return;
+			}
+			psg_write(psg, read_byte(player));
+			break;
+		case CMD_YM2612_0:
+			ym = find_chip_context(player, CMD_YM2612_0);
+			if (!ym || player->current_offset > player->media->size - 2) {
+				vgm_stop(player);
+				return;
+			}
+			ym_address_write_part1(ym, read_byte(player));
+			ym_data_write(ym, read_byte(player));
+			break;
+		case CMD_YM2612_1:
+			ym = find_chip_context(player, CMD_YM2612_0);
+			if (!ym || player->current_offset > player->media->size - 2) {
+				vgm_stop(player);
+				return;
+			}
+			ym_address_write_part2(ym, read_byte(player));
+			ym_data_write(ym, read_byte(player));
+			break;
+		case CMD_WAIT: {
+			if (player->current_offset > player->media->size - 2) {
+				vgm_stop(player);
+				return;
+			}
+			player->wait_samples += read_word_le(player);
+			break;
+		}
+		case CMD_WAIT_60:
+			player->wait_samples += 735;
+			break;
+		case CMD_WAIT_50:
+			player->wait_samples += 882;
+			break;
+		case CMD_END:
+			//TODO: loops
+			vgm_stop(player);
+			return;
+		case CMD_PCM_WRITE: {
+			if (player->current_offset > player->media->size - 11) {
+				vgm_stop(player);
+				return;
+			}
+			player->current_offset++; //skip compatibility command
+			uint8_t data_type = read_byte(player);
+			uint32_t read_offset = read_24_le(player);
+			uint32_t write_offset = read_24_le(player);
+			uint16_t size = read_24_le(player);
+			chip_info *chip = find_chip_by_data(player, data_type);
+			if (!chip || !chip->blocks) {
+				warning("Failed to find data block list for type %d\n", data_type);
+				break;
+			}
+			uint8_t *src = find_block(chip->blocks, read_offset, size);
+			if (!src) {
+				warning("Failed to find data offset %X with size %X for chip type %d\n", read_offset, size, data_type);
+				break;
+			}
+			switch (data_type)
+			{
+			case DATA_RF5C68:
+			case DATA_RF5C164:
+				pcm = chip->context;
+				write_offset |= pcm->ram_bank;
+				write_offset &= 0xFFFF;
+				if (size + write_offset > 0x10000) {
+					size = 0x10000 - write_offset;
+				}
+				memcpy(pcm->ram + write_offset, src, size);
+				break;
+			default:
+				warning("Unknown PCM write read_offset %X, write_offset %X, size %X\n", read_offset, write_offset, size);
+			}
+			break;
+		}
+		case CMD_PCM68_REG:
+			pcm = find_chip_context(player, CMD_PCM68_REG);
+			if (!pcm || player->current_offset > player->media->size - 2) {
+				vgm_stop(player);
+				return;
+			} else {
+				uint8_t reg = read_byte(player);
+				uint8_t value = read_byte(player);
+				rf5c164_write(pcm, reg, value);
+			}
+			break;
+		case CMD_PCM164_REG:
+			pcm = find_chip_context(player, CMD_PCM164_REG);
+			if (!pcm || player->current_offset > player->media->size - 2) {
+				vgm_stop(player);
+				return;
+			} else {
+				uint8_t reg = read_byte(player);
+				uint8_t value = read_byte(player);
+				rf5c164_write(pcm, reg, value);
+			}
+			break;
+		case CMD_PCM68_RAM:
+			pcm = find_chip_context(player, CMD_PCM68_REG);
+			if (!pcm || player->current_offset > player->media->size - 3) {
+				vgm_stop(player);
+				return;
+			} else {
+				uint16_t address = read_word_le(player);
+				address &= 0xFFF;
+				address |= 0x1000;
+				rf5c164_write(pcm, address, read_byte(player));
+			}
+			break;
+		case CMD_PCM164_RAM:
+			pcm = find_chip_context(player, CMD_PCM164_REG);
+			if (!pcm || player->current_offset > player->media->size - 3) {
+				vgm_stop(player);
+				return;
+			} else {
+				uint16_t address = read_word_le(player);
+				address &= 0xFFF;
+				address |= 0x1000;
+				rf5c164_write(pcm, address, read_byte(player));
+			}
+			break;
+		case CMD_DATA:
+			if (player->current_offset > player->media->size - 6) {
+				vgm_stop(player);
+				return;
+			} else {
+				player->current_offset++; //skip compat command
+				uint8_t data_type = read_byte(player);
+				uint32_t data_size = read_long_le(player);
+				if (data_size > player->media->size || player->current_offset > player->media->size - data_size) {
+					vgm_stop(player);
+					return;
+				}
+				chip_info *chip = find_chip_by_data(player, data_type);
+				if (chip) {
+					data_block **cur = &(chip->blocks);
+					while (*cur)
+					{
+						cur = &((*cur)->next);
+					}
+					*cur = calloc(1, sizeof(data_block));
+					(*cur)->size = data_size;
+					(*cur)->type = data_type;
+					(*cur)->data = ((uint8_t *)player->media->buffer) + player->current_offset;
+				} else {
+					fprintf(stderr, "Skipping data block with unrecognized type %X\n", data_type);
+				}
+				player->current_offset += data_size;
+			}
+			break;
+		case CMD_DATA_SEEK:
+			if (player->current_offset > player->media->size - 4) {
+				vgm_stop(player);
+				return;
+			} else {
+				uint32_t new_offset = read_long_le(player);
+				if (!player->ym_seek_block || new_offset < player->ym_seek_offset) {
+					chip_info *chip = find_chip(player, CMD_YM2612_0);
+					if (!chip) {
+						break;
+					}
+					player->ym_seek_block = chip->blocks;
+					player->ym_seek_offset = 0;
+					player->ym_block_offset = 0;
+				}
+				while (player->ym_seek_block && (player->ym_seek_offset - player->ym_block_offset + player->ym_seek_block->size) < new_offset)
+				{
+					player->ym_seek_offset += player->ym_seek_block->size - player->ym_block_offset;
+					player->ym_seek_block = player->ym_seek_block->next;
+					player->ym_block_offset = 0;
+				}
+				player->ym_block_offset += new_offset - player->ym_seek_offset;
+				player->ym_seek_offset = new_offset;
+			}
+			break;
+		default:
+			if (cmd >= CMD_WAIT_SHORT && cmd < (CMD_WAIT_SHORT + 0x10)) {
+				uint32_t wait_time = (cmd & 0xF) + 1;
+				player->wait_samples += wait_time;
+			} else if (cmd >= CMD_YM2612_DAC && cmd < CMD_DAC_STREAM_SETUP) {
+				if (player->ym_seek_block) {
+					ym = find_chip_context(player, CMD_YM2612_0);
+					ym_address_write_part1(ym, 0x2A);
+					ym_data_write(ym, player->ym_seek_block->data[player->ym_block_offset++]);
+					player->ym_seek_offset++;
+					if (player->ym_block_offset > player->ym_seek_block->size) {
+						player->ym_seek_block = player->ym_seek_block->next;
+						player->ym_block_offset = 0;
+					}
+				} else {
+					fputs("Encountered DAC write command but data seek pointer is invalid!\n", stderr);
+				}
+				player->wait_samples += cmd & 0xF;
+			} else {
+				warning("unimplemented command: %X at offset %X\n", cmd, player->current_offset);
+				vgm_stop(player);
+				return;
+			}
+		}
+	}
+}
+
+void wave_frame(media_player *player)
+{
+	render_sleep_ms(15);
+}
+
+void flac_frame(media_player *player)
+{
+	render_sleep_ms(15);
+}
+
+void vgm_init(media_player *player, uint32_t opts)
+{
+	player->vgm = calloc(1, sizeof(vgm_header));
+	player->vgm_ext = NULL;
+	memcpy(player->vgm, player->media->buffer, sizeof(vgm_header));
+	if (player->vgm->version < 0x150 || !player->vgm->data_offset) {
+		player->vgm->data_offset = 0xC;
+	}
+	if (player->vgm->data_offset + offsetof(vgm_header, data_offset) > player->media->size) {
+		player->vgm->data_offset = player->media->size - offsetof(vgm_header, data_offset);
+	}
+	if (player->vgm->version <= 0x101 && player->vgm->ym2413_clk > 4000000) {
+		player->vgm->ym2612_clk = player->vgm->ym2413_clk;
+		player->vgm->ym2413_clk = 0;
+	}
+	if (player->vgm->data_offset > 0xC) {
+		player->vgm_ext = calloc(1, sizeof(vgm_extended_header));
+		size_t additional_header = player->vgm->data_offset + offsetof(vgm_header, data_offset) - sizeof(vgm_header);
+		if (additional_header > sizeof(vgm_extended_header)) {
+			additional_header = sizeof(vgm_extended_header);
+		}
+		memcpy(player->vgm_ext, ((uint8_t *)player->media->buffer) + sizeof(vgm_header),  additional_header);
+	}
+	player->num_chips = 0;
+	if (player->vgm->sn76489_clk) {
+		player->num_chips++;
+	}
+	if (player->vgm->ym2612_clk) {
+		player->num_chips++;
+	}
+	if (player->vgm_ext && player->vgm_ext->rf5c68_clk) {
+		player->num_chips++;
+	}
+	if (player->vgm_ext && player->vgm_ext->rf5c164_clk) {
+		player->num_chips++;
+	}
+	player->chips = calloc(player->num_chips, sizeof(chip_info));
+	uint32_t chip = 0;
+	if (player->vgm->sn76489_clk) {
+		psg_context *psg = calloc(1, sizeof(psg_context));
+		psg_init(psg, player->vgm->sn76489_clk, 1);
+		player->chips[chip++] = (chip_info) {
+			.context = psg,
+			.run = (chip_run_fun)psg_run,
+			.adjust = psg_adjust,
+			.clock = player->vgm->sn76489_clk,
+			.samples = 0,
+			.cmd = CMD_PSG,
+			.data_type = 0xFF
+		};
+	}
+	if (player->vgm->ym2612_clk) {
+		ym2612_context *ym = calloc(1, sizeof(ym2612_context));
+		ym_init(ym, player->vgm->ym2612_clk, 1, opts);
+		player->chips[chip++] = (chip_info) {
+			.context = ym,
+			.run = (chip_run_fun)ym_run,
+			.adjust = ym_adjust,
+			.clock = player->vgm->ym2612_clk,
+			.samples = 0,
+			.cmd = CMD_YM2612_0,
+			.data_type = DATA_YM2612_PCM
+		};
+	}
+	if (player->vgm_ext && player->vgm_ext->rf5c68_clk) {
+		rf5c164 *pcm = calloc(1, sizeof(rf5c164));
+		rf5c164_init(pcm, player->vgm_ext->rf5c68_clk, 1);
+		player->chips[chip++] = (chip_info) {
+			.context = pcm,
+			.run = (chip_run_fun)rf5c164_run,
+			.adjust = pcm_adjust,
+			.clock = player->vgm_ext->rf5c68_clk,
+			.samples = 0,
+			.cmd = CMD_PCM68_REG,
+			.data_type = DATA_RF5C68
+		};
+	}
+	if (player->vgm_ext && player->vgm_ext->rf5c164_clk) {
+		rf5c164 *pcm = calloc(1, sizeof(rf5c164));
+		rf5c164_init(pcm, player->vgm_ext->rf5c164_clk, 1);
+		player->chips[chip++] = (chip_info) {
+			.context = pcm,
+			.run = (chip_run_fun)rf5c164_run,
+			.adjust = pcm_adjust,
+			.clock = player->vgm_ext->rf5c164_clk,
+			.samples = 0,
+			.cmd = CMD_PCM164_REG,
+			.data_type = DATA_RF5C164
+		};
+	}
+	player->current_offset = player->vgm->data_offset + offsetof(vgm_header, data_offset);
+}
+
+static void resume_player(system_header *system)
+{
+	media_player *player = (media_player *)system;
+	player->should_return = 0;
+	while (!player->header.should_exit && !player->should_return)
+	{
+		switch (player->state)
+		{
+		case STATE_PLAY:
+			switch(player->media_type)
+			{
+			case AUDIO_VGM:
+				vgm_frame(player);
+				break;
+			case AUDIO_WAVE:
+				wave_frame(player);
+				break;
+			case AUDIO_FLAC:
+				flac_frame(player);
+				break;
+			}
+			break;
+		case STATE_PAUSED:
+			render_sleep_ms(15);
+			break;
+		}
+		render_update_display();
+	}
+}
+
+static void gamepad_down(system_header *system, uint8_t pad, uint8_t button)
+{
+	if (button >= BUTTON_A && button <= BUTTON_C) {
+		media_player *player = (media_player *)system;
+		if (player->state == STATE_PAUSED) {
+			player->state = STATE_PLAY;
+			puts("Now playing");
+		} else {
+			player->state = STATE_PAUSED;
+			puts("Now paused");
+		}
+	}
+}
+
+static void gamepad_up(system_header *system, uint8_t pad, uint8_t button)
+{
+}
+
+static void start_player(system_header *system, char *statefile)
+{
+	resume_player(system);
+}
+
+static void free_player(system_header *system)
+{
+	media_player *player = (media_player *)system;
+	for (uint32_t i = 0; i < player->num_chips; i++)
+	{
+		//TODO properly free chips
+		free(player->chips[i].context);
+	}
+	free(player->chips);
+	free(player->vgm);
+	free(player);
+}
+
+uint8_t detect_media_type(system_media *media)
+{
+	if (media->size < 4) {
+		return MEDIA_UNKNOWN;
+	}
+	if (!memcmp(media->buffer, "Vgm ", 4)) {
+		if (media->size < sizeof(vgm_header)) {
+			return MEDIA_UNKNOWN;
+		}
+		return AUDIO_VGM;
+	}
+	if (!memcmp(media->buffer, "RIFF", 4)) {
+		return AUDIO_WAVE;
+	}
+	if (!memcmp(media->buffer, "fLaC", 4)) {
+		return AUDIO_FLAC;
+	}
+	return MEDIA_UNKNOWN;
+}
+
+static void request_exit(system_header *system)
+{
+	media_player *player = (media_player *)system;
+	player->should_return = 1;
+}
+
+media_player *alloc_media_player(system_media *media, uint32_t opts)
+{
+	media_player *player = calloc(1, sizeof(media_player));
+	player->header.start_context = start_player;
+	player->header.resume_context = resume_player;
+	player->header.request_exit = request_exit;
+	player->header.free_context = free_player;
+	player->header.gamepad_down = gamepad_down;
+	player->header.gamepad_up = gamepad_down;
+	player->header.type = SYSTEM_MEDIA_PLAYER;
+	player->header.info.name = strdup(media->name);
+
+	player->media = media;
+	player->media_type = detect_media_type(media);
+	player->state = STATE_PLAY;
+	switch (player->media_type)
+	{
+	case AUDIO_VGM:
+		vgm_init(player, opts);
+		break;
+	}
+
+	return player;
+}