/* SOUND.C ======= Sound chip and cassette routines. */ #include #include "v9t9_common.h" #include "timer.h" #include "sound.h" #include "demo.h" #include "command.h" #define _L LOG_SOUND | LOG_INFO /* I've read from ti99sim that this is really logarithmic */ static u8 attenuation_to_volume[16] = { 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, 0x00 }; voiceinfo sound_voices[4]; /* The current voice (selected by writing OPERATION_FREQUENCY_LO byte). May be useful for arpeggiation. */ static u8 cvoice; int sndplayhz = 44100; /* playback rate */ int sndplaybits = 8; /* word size for sound */ /*************************************/ static u16 noise_period[4] = { 16, 32, 64, 0 /* determined by VOICE_TONE_2 */ }; /* Update cached fields */ static void voice_cache_values(voiceinfo *v) { if (v != &sound_voices[VOICE_NOISE]) { v->volume = OPERATION_TO_VOLUME(v); v->period = OPERATION_TO_PERIOD(v); v->hertz = PERIOD_TO_HERTZ(v->period); } else { int period = OPERATION_TO_NOISE_PERIOD(v); int type = OPERATION_TO_NOISE_TYPE(v); if (period != NOISE_PERIOD_VARIABLE) { v->period = noise_period[period]; v->hertz = PERIOD_TO_HERTZ(v->period); } else { v->period = sound_voices[VOICE_TONE_2].period; v->hertz = sound_voices[VOICE_TONE_2].hertz; } } logger(_L|L_3, "voice_cache_values: lo=>%02x, hi=>%02x, period=>%04x, hertz=%d, volume=%d\n", v->operation[OPERATION_FREQUENCY_LO], v->operation[OPERATION_FREQUENCY_HI], v->period, v->hertz, v->volume); } /* * Update the noise channel when a noise operation occurs * or VOICE_TONE_2 changes and the noise control is NOISE_PERIOD_VARIABLE. */ static void sound_update_noise(void) { voiceinfo *v = &sound_voices[VOICE_NOISE]; int period = OPERATION_TO_NOISE_PERIOD(v); if ((cvoice == VOICE_TONE_2 && period == NOISE_PERIOD_VARIABLE) || cvoice == VOICE_NOISE) { SOUNDUPDATE(vms_Tn); } } void sound_mmio_write(u8 val) { voiceinfo *v; int vn; if (stateflag & ST_DEMOING) { demo_record_event(demo_type_sound, val); } /* handle command byte */ if (val & 0x80) { vn = OPERATION_TO_VOICE(val); cvoice = vn; v = &sound_voices[vn]; switch ((val & 0x70) >> 4) { case 0: /* T1 FRQ */ case 2: /* T2 FRQ */ case 4: /* T3 FRQ */ v->operation[OPERATION_FREQUENCY_LO] = val; voice_cache_values(v); sound_update_noise(); SOUNDUPDATE(vms_Tv0 << vn); break; case 1: /* T1 ATT */ case 3: /* T2 ATT */ case 5: /* T3 ATT */ v->operation[OPERATION_ATTENUATION] = val; voice_cache_values(v); SOUNDUPDATE(vms_Vv0 << vn); break; case 6: /* noise ctl */ v->operation[OPERATION_CONTROL] = val; sound_update_noise(); break; case 7: /* noise vol */ v->operation[OPERATION_ATTENUATION] = val; voice_cache_values(v); SOUNDUPDATE(vms_Vn); break; } } /* second frequency byte */ else if (!(val & 0xc0)) { v = &sound_voices[cvoice]; v->operation[OPERATION_FREQUENCY_HI] = val; voice_cache_values(v); sound_update_noise(); SOUNDUPDATE(vms_Tv0 << cvoice); } } /********************************************/ /* Cassette routines */ static vmModule *cs[2]; static u8 cs_open[2]; static u16 cs_clock; static u8 cs_motor[2]; static u32 cs_lasttime; // last clock when data was read (3.0/64 mhz) static u8 cs_latch, // last data read cs_min, cs_max; // midrange of incoming data static void cassette_open(int csx) { vmModule *ptr; if (csx < 0 || csx >= 2) return; if (cs_open[csx]) return; /* Select where to filter cassette info; for now, just use sound module */ for (ptr = vmSound; ptr; ptr = ptr->next) { // cs2 [csx=1] can only write, methinks (try "OLD CS2") if (ptr->m.sound->play && (csx != 1 || ptr->m.sound->read) && !(ptr->runtimeflags & vmRTUnselected)) { break; } } if (ptr) logger(_L | 0, "Cassette %d is %p (%s)\n\n", csx, ptr, ptr->name); cs[csx] = ptr; cs_open[csx] = (ptr != NULL); } void cassette_set_timer(u16 hz) { cs_clock = hz; } void cassette_set_motor(int csx, u8 onoff) { if (csx < 0 || csx >= 2) return; cs_motor[csx] = onoff; cs_lasttime = 0; cs_min = 0xff; cs_max = 0; cs_latch = 0x80; if (onoff) cassette_open(csx); } void cassette_write(u8 onoff, u32 timepassed) { // we should not write to the sound module because // the mix server is usually owning it. /* u8 *data = (u8 *)alloca(4096); memset(data, onoff ? 0x7f : 0x00, 4096); while (timepassed) { u32 quant = (timepassed >= 4096 ? 4096 : timepassed); #warning can we write to both at once? if (cs_motor[0]) cs[0]->m.sound->play(vms_CS, data, quant, cs_clock ? cs_clock : baseclockhz / 64); if (cs_motor[1]) cs[1]->m.sound->play(vms_CS, data, quant, cs_clock ? cs_clock : baseclockhz / 64); timepassed -= quant; } */ } u8 cassette_read(void) { u32 timepassed; u8 *data; u32 average; if (currenttime >= cs_lasttime) timepassed = currenttime - cs_lasttime; else timepassed = 64; if (cs_clock) timepassed = (timepassed + cs_clock - 1) * (cs_clock) / (baseclockhz / 64); else timepassed = timepassed / 64; data = (u8 *) alloca(timepassed + 1); average = 0; if (timepassed && cs_open[0]) { cs_lasttime = currenttime; // read at most as much data as represented by cs_clock if (cs[0]->m.sound->read) cs[0]->m.sound->read(vms_AGr, data, timepassed, cs_clock ? cs_clock : baseclockhz / 64); while (timepassed--) average = (average + data[timepassed]) / 2; cs_latch = average; if (cs_latch < cs_min) cs_min = cs_latch; else if (cs_min < 0x60 / 2) cs_min++; if (cs_latch > cs_max) cs_max = cs_latch; else if (cs_max > 0xa0 / 2) cs_max--; } // else, use last cs_latch logger(_L | L_2, "cs = %d/%d/%d = \n", cs_min, cs_latch, cs_max); if (cs_latch > cs_min + (cs_max - cs_min) / 2) { logger(_L | L_2, "1\n"); return 1; } else { logger(_L | L_2, "0\n\n"); return 0; } } /********************************************/ static DECL_SYMBOL_ACTION(sound_playsound_toggle) { if (features & FE_PLAYSOUND) { DOSOUNDUPDATE(vms_Tv0 | vms_Tv1 | vms_Tv2 | vms_Tn | vms_Vv0 | vms_Vv1 | vms_Vv2 | vms_Vn); } else { DOSOUNDFLUSH(); } return 1; } static DECL_SYMBOL_ACTION(sound_enablesound_toggle) { if (features & FE_SOUND) { sound_enable(); sound_restart(); } else { sound_restop(); sound_disable(); } return 1; } static DECL_SYMBOL_ACTION(sound_module_restart) { if (sndplaybits <= 8) sndplaybits = 8; else sndplaybits = 16; sound_restop(); sound_restart(); return 1; } static DECL_SYMBOL_ACTION(sound_config_state) { struct voiceinfo *v; int val; if (task == csa_READ) { if (iter >= 4) return 0; v = &sound_voices[iter]; command_arg_set_num(SYM_ARG_1st, iter); command_arg_set_num(SYM_ARG_2nd, v->operation[OPERATION_FREQUENCY_LO]); command_arg_set_num(SYM_ARG_3rd, v->operation[OPERATION_FREQUENCY_HI]); command_arg_set_num(SYM_ARG_4th, v->operation[OPERATION_ATTENUATION]); return 1; } command_arg_get_num(SYM_ARG_1st, &val); if (val < 0 || val >= 4) { logger(_L | LOG_USER | LOG_ERROR, "Invalid sound channel specified (%d)\n", val); return 0; } iter = val; v = &sound_voices[iter]; command_arg_get_num(SYM_ARG_2nd, &val); v->operation[OPERATION_FREQUENCY_LO] = val; command_arg_get_num(SYM_ARG_3rd, &val); v->operation[OPERATION_FREQUENCY_HI] = val; command_arg_get_num(SYM_ARG_4th, &val); v->operation[OPERATION_ATTENUATION] = val; voice_cache_values(v); sound_update_noise(); SOUNDUPDATE((vms_Tv0|vms_Vv0) << iter); return 1; } void sound_init(void) { command_symbol_table *soundcommands = command_symbol_table_new("Sound Options", "These are generic commands for controlling sound emulation", command_symbol_new("PlaySound", "Control whether music/noise sound is played.\n" "(Note: to turn off all sound, " "disable the sound module or use \"EnableSound off\".)", c_STATIC, sound_playsound_toggle, RET_FIRST_ARG, command_arg_new_toggle ("on|off", "toggle sound on or off", NULL /* action */ , ARG_NUM(features), FE_PLAYSOUND, NULL /* next */ ) , command_symbol_new("EnableSound", "Control whether any sound is emitted.", c_STATIC, sound_enablesound_toggle, RET_FIRST_ARG, command_arg_new_toggle ("on|off", "toggle sound on or off", NULL /* action */ , ARG_NUM(features), FE_SOUND, NULL /* next */ ) , command_symbol_new("DigitalSoundHertz", "Set playback rate for digitized sound; " "interpretation is dependent on sound module in effect", c_STATIC, sound_module_restart, RET_FIRST_ARG, command_arg_new_num("Hz", "set rate for playback", NULL /* action */, ARG_NUM(sndplayhz), NULL /* next */), command_symbol_new("DigitalSoundBits", "Set word size for digitized sound; " "interpretation is dependent on sound module in effect", c_STATIC, sound_module_restart, RET_FIRST_ARG, command_arg_new_num("8|16", "set word size for playback", NULL /* action */ , ARG_NUM(sndplaybits), NULL /* next */ ) , command_symbol_new("SoundChannelState", NULL /* help */, c_DYNAMIC|c_SESSION_ONLY, sound_config_state, NULL /* ret */, command_arg_new_num("channel", "channel number, 0-3", NULL /* action */, NEW_ARG_NUM(u8), command_arg_new_num("period", "period of channel", NULL /* action */, NEW_ARG_NUM(u16), command_arg_new_num("volume", "volume 0-15, 15=off", NULL /* action */, NEW_ARG_NUM(u8), command_arg_new_num("type", "type of noise, or 0 for tone", NULL /* action */, NEW_ARG_NUM(u8), NULL)))), NULL /* next */ ))))), NULL /* sub */ , NULL /* next */ ); int v; command_symbol_table_add_subtable(universe, soundcommands); features |= FE_PLAYSOUND | FE_PLAYSPEECH; for (v=0; v<4; v++) { sound_voices[v].operation[OPERATION_ATTENUATION] = 0xf; voice_cache_values(&sound_voices[v]); } cvoice = 0; } int sound_restart(void) { if (features & FE_PLAYSOUND) { DOSOUNDUPDATE(vms_Tv0 | vms_Tv1 | vms_Tv2 | vms_Tn | vms_Vv0 | vms_Vv1 | vms_Vv2 | vms_Vn); } if (features & FE_PLAYSPEECH) { DOSOUNDPLAY(vms_Speech, NULL, 0, 0); } return 1; } void sound_restop(void) { SOUNDFLUSH(); } int sound_enable(void) { vmModule *ptr = vmSound; while (ptr) { if (ptr->runtimeflags & vmRTInUse) { if (vmEnableModule(ptr) != vmOk) return 0; } ptr = ptr->next; } return 1; } void sound_disable(void) { vmModule *ptr = vmSound; while (ptr) { if (ptr->runtimeflags & vmRTInUse) { vmDisableModule(ptr); } ptr = ptr->next; } }