/* * mii_speaker.c * * Copyright (C) 2023 Michel Pollet * * SPDX-License-Identifier: MIT */ #include #include #include #include #include "mii.h" #include "mii_speaker.h" // one frame of audio per frame of video? #define MII_SPEAKER_FRAME_SIZE (MII_SPEAKER_FREQ / 60) // TODO Make some sort of driver for audio and move alsa code there #ifdef HAS_ALSA #include #define PCM_DEVICE "default" static int _alsa_init( mii_speaker_t *s) { int pcm; unsigned int rate = 44100, channels = 1; snd_pcm_t *alsa; snd_pcm_hw_params_t *params; snd_pcm_uframes_t frames; /* Open the PCM device in playback mode */ if ((pcm = snd_pcm_open(&alsa, PCM_DEVICE, SND_PCM_STREAM_PLAYBACK, 0)) < 0) printf("ERROR: Can't open \"%s\" PCM device. %s\n", PCM_DEVICE, snd_strerror(pcm)); /* Allocate parameters object and fill it with default values*/ snd_pcm_hw_params_alloca(¶ms); snd_pcm_hw_params_any(alsa, params); /* Set parameters */ if ((pcm = snd_pcm_hw_params_set_access(alsa, params, SND_PCM_ACCESS_RW_INTERLEAVED)) < 0) printf("ERROR: Can't set interleaved mode. %s\n", snd_strerror(pcm)); if ((pcm = snd_pcm_hw_params_set_format(alsa, params, SND_PCM_FORMAT_S16_LE)) < 0) printf("ERROR: Can't set format. %s\n", snd_strerror(pcm)); if ((pcm = snd_pcm_hw_params_set_channels(alsa, params, channels)) < 0) printf("ERROR: Can't set channels number. %s\n", snd_strerror(pcm)); if ((pcm = snd_pcm_hw_params_set_rate_near(alsa, params, &rate, 0)) < 0) printf("ERROR: Can't set rate. %s\n", snd_strerror(pcm)); frames = MII_SPEAKER_FRAME_SIZE; /* Write parameters */ if ((pcm = snd_pcm_hw_params(alsa, params)) < 0) printf("ERROR: Can't set harware parameters. %s\n", snd_strerror(pcm)); // printf("%s frames want %d got %ld\n", // __func__, MII_SPEAKER_FRAME_SIZE, frames); snd_pcm_sw_params_t *sw_params; snd_pcm_sw_params_alloca (&sw_params); snd_pcm_sw_params_current (alsa, sw_params); snd_pcm_sw_params_set_start_threshold(alsa, sw_params, frames * 4); snd_pcm_sw_params_set_avail_min(alsa, sw_params, frames*4); snd_pcm_sw_params(alsa, sw_params); s->fsize = frames; s->alsa_pcm = alsa; snd_pcm_prepare(s->alsa_pcm); return 0; } #endif static uint64_t _mii_speaker_timer_cb( mii_t * mii, void * param ); // Initialize the speaker with the frame size in samples void mii_speaker_init( struct mii_t * mii, mii_speaker_t *s) { s->mii = mii; s->debug_fd = -1; s->fsize = MII_SPEAKER_FRAME_SIZE; // disabled at start... s->timer_id = mii_timer_register(mii, _mii_speaker_timer_cb, s, 0, __func__); #ifdef HAS_ALSA if (!s->off) _alsa_init(s); // this can/will change fsize #endif mii_speaker_volume(s, 1); s->sample = 0x8000; s->findex = 0; for (int i = 0; i < MII_SPEAKER_FRAME_COUNT; i++) s->frame[i].audio = calloc(sizeof(s->frame[i].audio[0]), s->fsize); // s->frame[0].start = mii->cycles; } void mii_speaker_dispose( mii_speaker_t *s) { s->fsize = 0; mii_timer_set(s->mii, s->timer_id, 0); #ifdef HAS_ALSA if (s->alsa_pcm) snd_pcm_close(s->alsa_pcm); #endif for (int i = 0; i < MII_SPEAKER_FRAME_COUNT; i++) { free(s->frame[i].audio); s->frame[i].audio = NULL; } } // Check to see if there's a new frame to send, send it // this timer is always running; it keeps checking for non-empty frames static uint64_t _mii_speaker_timer_cb( mii_t * mii, void * param ) { mii_speaker_t *s = (mii_speaker_t *)param; if (s->muted || s->off) goto done; mii_audio_frame_t *f = &s->frame[s->fplay]; // if the frame is empty, we mark the fact we are in underrun, // so we can restart the audio later on. if (!f->fill) { if (s->under < 10) s->under++; goto done; } s->under = 0; // Here we got a frame to play, so we play it, and move on to the next // There's also the case were we stopped playing and the last frame // wasn't complete, in which case we pad it, and flush it as well // printf("%s: fplay %d findex %d fsize %d fill %d\n", // __func__, s->fplay, s->findex, s->fsize, f->fill); uint16_t sample = f->audio[f->fill - 1] ^ 0xffff; while (f->fill < s->fsize) f->audio[f->fill++] = sample; s->fplay = (s->fplay + 1) % MII_SPEAKER_FRAME_COUNT; s->frame[s->fplay].fill = 0; if (!s->muted) { if (s->debug_fd != -1) write(s->debug_fd, f->audio, f->fill * sizeof(s->frame[0].audio[0])); #ifdef HAS_ALSA if (s->alsa_pcm) { int pcm; if ((pcm = snd_pcm_writei(s->alsa_pcm, f->audio, f->fill)) == -EPIPE) { printf("%s Underrun.\n", __func__); snd_pcm_recover(s->alsa_pcm, pcm, 1); } } #endif } done: return s->fsize * s->clk_per_sample; } // Called when $c030 is touched, place a sample at the 'appropriate' time void mii_speaker_click( mii_speaker_t *s) { // if CPU speed has changed, recalculate the number of cycles per sample if (s->cpu_speed != s->mii->speed) { s->cpu_speed = s->mii->speed; s->clk_per_sample = ((1000000.0 /* / s->mii->speed */) / (float)MII_SPEAKER_FREQ) + 0.5f; printf("%s: %.2f cycles per sample\n", __func__, s->clk_per_sample); mii_timer_set(s->mii, s->timer_id, s->fsize * s->clk_per_sample); } int64_t remains = mii_timer_get(s->mii, s->timer_id); mii_audio_frame_t *f = &s->frame[s->findex]; // if we had stopped playing for 2 frames, restart if (s->under > 1) { s->under = 0; // printf("Restarting playback\n"); #ifdef HAS_ALSA if (s->alsa_pcm) snd_pcm_prepare(s->alsa_pcm); #endif f->fill = 0; // add a small attack to the start of the frame to soften the beeps // we are going to flip the sample, so we need to preemptively // flip the attack as well mii_audio_sample_t attack = s->sample ^ 0xffff; for (int i = 8; i >= 1; i--) f->audio[f->fill++] = (attack / i) * s->vol_multiplier; s->fplay = s->findex; // restart here mii_timer_set(s->mii, s->timer_id, (s->fsize - 16) * s->clk_per_sample); remains = mii_timer_get(s->mii, s->timer_id); } // calculate the sample index we are going to fill -- this is relative // to the frame we are waiting to play long sample_index = // (s->fsize / 2) + (((s->fsize * s->clk_per_sample) - remains) / s->clk_per_sample); // fill from last sample to here with the current sample for (; f->fill < sample_index && f->fill < s->fsize; f->fill++) f->audio[f->fill] = s->sample * s->vol_multiplier; // printf("%s: findex %d fsize %d fill %d sample_index %ld\n", // __func__, s->findex, s->fsize, f->fill, sample_index); // if we've gone past the end of the frame, switch to the next one if (sample_index >= s->fsize || sample_index < f->fill) { sample_index = sample_index % s->fsize; s->findex = (s->findex + 1) % MII_SPEAKER_FRAME_COUNT; f = &s->frame[s->findex]; f->fill = 0; // fill from start of this frame to newly calculated sample_index for (; f->fill < sample_index && f->fill < s->fsize; f->fill++) f->audio[f->fill] = s->sample * s->vol_multiplier; f->audio[sample_index] = 0; } s->sample ^= 0xffff; // if we are touching a new sample, make sure the next one is clear too. #if 1 int32_t mix = s->sample * s->vol_multiplier; #else /* Mixing code; in case theres multiple clicks within a sample period. * This is not used, because it's not really needed, as 2 clicks * would cancel each others anyway. */ if (!f->audio[sample_index] && sample_index < s->fsize) f->audio[sample_index + 1] = 0; int32_t mix = f->audio[sample_index] + (s->sample * s->vol_multiplier); if (mix > 0x7fff) mix = 0x7fff; else if (mix < -0x8000) mix = -0x8000; #endif f->audio[sample_index] = mix; } // this is here so we dont' have to drag in libm math library. double fastPow(double a, double b) { union { double d; int32_t x[2]; } u = { .d = a }; u.x[1] = (int)(b * (u.x[1] - 1072632447) + 1072632447); u.x[0] = 0; return u.d; } // take the volume from 0 to 10, save it, convert it to a multiplier void mii_speaker_volume( mii_speaker_t *s, float volume) { if (volume < 0) volume = 0; else if (volume > 10) volume = 10; double mul = (fastPow(10.0, volume / 10.0) / 10.0) - 0.09; s->vol_multiplier = mul; s->volume = volume; // printf("audio: speaker volume set to %.3f (%.4f)\n", volume, mul); }