/************************************************************************* laserdsc.c Core laserdisc player implementation. **************************************************************************** Copyright Aaron Giles All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name 'MAME' nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY AARON GILES ''AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL AARON GILES BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. *************************************************************************/ #include "emu.h" #include "laserdsc.h" #include "avhuff.h" #include "vbiparse.h" #include "config.h" #include "render.h" #include "chd.h" //************************************************************************** // DEBUGGING //************************************************************************** #define LOG_SLIDER 0 //************************************************************************** // CONSTANTS //************************************************************************** // these specs code from IEC 60857, for NTSC players const UINT32 LEAD_IN_MIN_RADIUS_IN_UM = 53500; // 53.5 mm const UINT32 PROGRAM_MIN_RADIUS_IN_UM = 55000; // 55 mm const UINT32 PROGRAM_MAX_RADIUS_IN_UM = 145000; // 145 mm const UINT32 LEAD_OUT_MIN_SIZE_IN_UM = 2000; // 2 mm // the track pitch is defined as a range; we pick a nominal pitch // that ensures we can fit 54,000 tracks const UINT32 MIN_TRACK_PITCH_IN_NM = 1400; // 1.4 um const UINT32 MAX_TRACK_PITCH_IN_NM = 2000; // 2 um const UINT32 NOMINAL_TRACK_PITCH_IN_NM = (PROGRAM_MAX_RADIUS_IN_UM - PROGRAM_MIN_RADIUS_IN_UM) * 1000 / 54000; // we simulate extra lead-in and lead-out tracks const UINT32 VIRTUAL_LEAD_IN_TRACKS = (PROGRAM_MIN_RADIUS_IN_UM - LEAD_IN_MIN_RADIUS_IN_UM) * 1000 / NOMINAL_TRACK_PITCH_IN_NM; const UINT32 MAX_TOTAL_TRACKS = 54000; const UINT32 VIRTUAL_LEAD_OUT_TRACKS = LEAD_OUT_MIN_SIZE_IN_UM * 1000 / NOMINAL_TRACK_PITCH_IN_NM; //************************************************************************** // CORE IMPLEMENTATION //************************************************************************** //------------------------------------------------- // laserdisc_device - constructor //------------------------------------------------- laserdisc_device::laserdisc_device(const machine_config &mconfig, device_type type, const char *name, const char *tag, device_t *owner, UINT32 clock, const char *shortname, const char *source) : device_t(mconfig, type, name, tag, owner, clock, shortname, source), device_sound_interface(mconfig, *this), m_screen_name(NULL), m_overwidth(0), m_overheight(0), m_overclip(0, -1, 0, -1), m_disc(NULL), m_width(0), m_height(0), m_fps_times_1million(0), m_samplerate(0), m_readresult(CHDERR_NONE), m_chdtracks(0), m_work_queue(osd_work_queue_alloc(WORK_QUEUE_FLAG_IO)), m_audiosquelch(0), m_videosquelch(0), m_fieldnum(0), m_curtrack(0), m_maxtrack(0), m_attospertrack(0), m_sliderupdate(attotime::zero), m_videoindex(0), m_stream(NULL), m_audiobufsize(0), m_audiobufin(0), m_audiobufout(0), m_audiocursamples(0), m_audiomaxsamples(0), m_videoenable(false), m_videotex(NULL), m_videopalette(NULL), m_overenable(false), m_overindex(0), m_overtex(NULL) { // initialize overlay_config m_orig_config.m_overposx = m_orig_config.m_overposy = 0.0f; m_orig_config.m_overscalex = m_orig_config.m_overscaley = 1.0f; *static_cast(this) = m_orig_config; } //------------------------------------------------- // ~laserdisc_device - destructor //------------------------------------------------- laserdisc_device::~laserdisc_device() { osd_work_queue_free(m_work_queue); } //************************************************************************** // PUBLIC INTERFACES //************************************************************************** //------------------------------------------------- // get_field_code - return raw field information // read from the disc //------------------------------------------------- UINT32 laserdisc_device::get_field_code(laserdisc_field_code code, bool zero_if_squelched) { // return nothing if the video is off (external devices can't sense) if (zero_if_squelched && m_videosquelch) return 0; switch (code) { case LASERDISC_CODE_WHITE_FLAG: return m_metadata[m_fieldnum].white; case LASERDISC_CODE_LINE16: return m_metadata[m_fieldnum].line16; case LASERDISC_CODE_LINE17: return m_metadata[m_fieldnum].line17; case LASERDISC_CODE_LINE18: return m_metadata[m_fieldnum].line18; case LASERDISC_CODE_LINE1718: return m_metadata[m_fieldnum].line1718; } return 0; } //------------------------------------------------- // screen_update - handle updating the screen //------------------------------------------------- UINT32 laserdisc_device::screen_update(screen_device &screen, bitmap_rgb32 &bitmap, const rectangle &cliprect) { // handle the overlay if present screen_bitmap &overbitmap = m_overbitmap[m_overindex]; if (overbitmap.valid() && (!m_overupdate_ind16.isnull() || !m_overupdate_rgb32.isnull())) { // scale the cliprect to the overlay size rectangle clip(m_overclip); clip.min_y = cliprect.min_y * overbitmap.height() / bitmap.height(); if (cliprect.min_y == screen.visible_area().min_y) clip.min_y = MIN(clip.min_y, m_overclip.min_y); clip.max_y = (cliprect.max_y + 1) * overbitmap.height() / bitmap.height() - 1; // call the update callback if (!m_overupdate_ind16.isnull()) m_overupdate_ind16(screen, overbitmap.as_ind16(), clip); else m_overupdate_rgb32(screen, overbitmap.as_rgb32(), clip); } // if this is the last update, do the rendering if (cliprect.max_y == screen.visible_area().max_y) { // update the texture with the overlay contents if (overbitmap.valid()) m_overtex->set_bitmap(overbitmap, m_overclip, overbitmap.texformat()); // get the laserdisc video bitmap_yuy16 &vidbitmap = get_video(); m_videotex->set_bitmap(vidbitmap, vidbitmap.cliprect(), TEXFORMAT_YUY16); // reset the screen contents screen.container().empty(); // add the video texture if (m_videoenable) screen.container().add_quad(0.0f, 0.0f, 1.0f, 1.0f, MAKE_ARGB(0xff,0xff,0xff,0xff), m_videotex, PRIMFLAG_BLENDMODE(BLENDMODE_NONE) | PRIMFLAG_SCREENTEX(1)); // add the overlay if (m_overenable && overbitmap.valid()) { float x0 = 0.5f - 0.5f * m_overscalex + m_overposx; float y0 = 0.5f - 0.5f * m_overscaley + m_overposy; float x1 = x0 + m_overscalex; float y1 = y0 + m_overscaley; screen.container().add_quad(x0, y0, x1, y1, MAKE_ARGB(0xff,0xff,0xff,0xff), m_overtex, PRIMFLAG_BLENDMODE(BLENDMODE_ALPHA) | PRIMFLAG_SCREENTEX(1)); } // swap to the next bitmap m_overindex = (m_overindex + 1) % ARRAY_LENGTH(m_overbitmap); } return 0; } //------------------------------------------------- // static_set_screen - set the screen name //------------------------------------------------- void laserdisc_device::static_set_screen(device_t &device, const char *screen) { downcast(device).m_screen_name = screen; } //------------------------------------------------- // static_set_get_disc - set the get disc // delegate //------------------------------------------------- void laserdisc_device::static_set_get_disc(device_t &device, laserdisc_get_disc_delegate callback) { downcast(device).m_getdisc_callback = callback; } //------------------------------------------------- // static_set_get_disc - set the audio interceptor // delegate //------------------------------------------------- void laserdisc_device::static_set_audio(device_t &device, laserdisc_audio_delegate callback) { downcast(device).m_audio_callback = callback; } //------------------------------------------------- // static_set_overlay - set the overlay parameters //------------------------------------------------- void laserdisc_device::static_set_overlay(device_t &device, UINT32 width, UINT32 height, screen_update_ind16_delegate update) { laserdisc_device &ld = downcast(device); ld.m_overwidth = width; ld.m_overheight = height; ld.m_overclip.set(0, width - 1, 0, height - 1); ld.m_overupdate_ind16 = update; ld.m_overupdate_rgb32 = screen_update_rgb32_delegate(); } void laserdisc_device::static_set_overlay(device_t &device, UINT32 width, UINT32 height, screen_update_rgb32_delegate update) { laserdisc_device &ld = downcast(device); ld.m_overwidth = width; ld.m_overheight = height; ld.m_overclip.set(0, width - 1, 0, height - 1); ld.m_overupdate_ind16 = screen_update_ind16_delegate(); ld.m_overupdate_rgb32 = update; } //------------------------------------------------- // static_set_overlay - set the overlay visible // memregion //------------------------------------------------- void laserdisc_device::static_set_overlay_clip(device_t &device, INT32 minx, INT32 maxx, INT32 miny, INT32 maxy) { downcast(device).m_overclip.set(minx, maxx, miny, maxy); } //------------------------------------------------- // static_set_overlay_position - set the overlay // position parameters //------------------------------------------------- void laserdisc_device::static_set_overlay_position(device_t &device, float posx, float posy) { laserdisc_device &ld = downcast(device); ld.m_orig_config.m_overposx = ld.m_overposx = posx; ld.m_orig_config.m_overposy = ld.m_overposy = posy; } //------------------------------------------------- // static_set_overlay_scale - set the overlay // scale parameters //------------------------------------------------- void laserdisc_device::static_set_overlay_scale(device_t &device, float scalex, float scaley) { laserdisc_device &ld = downcast(device); ld.m_orig_config.m_overscalex = ld.m_overscalex = scalex; ld.m_orig_config.m_overscaley = ld.m_overscaley = scaley; } //************************************************************************** // DEVICE INTERFACE //************************************************************************** //------------------------------------------------- // device start callback //------------------------------------------------- void laserdisc_device::device_start() { // ensure that our screen is started first m_screen = machine().device(m_screen_name); assert(m_screen != NULL); if (!m_screen->started()) throw device_missing_dependencies(); // initialize the various pieces init_disc(); init_video(); init_audio(); // register callbacks config_register(machine(), "laserdisc", config_saveload_delegate(FUNC(laserdisc_device::config_load), this), config_saveload_delegate(FUNC(laserdisc_device::config_save), this)); } //------------------------------------------------- // device stop callback //------------------------------------------------- void laserdisc_device::device_stop() { // make sure all async operations have completed if (m_disc != NULL) osd_work_queue_wait(m_work_queue, osd_ticks_per_second() * 10); // free any textures and palettes if (m_videotex != NULL) machine().render().texture_free(m_videotex); if (m_videopalette != NULL) palette_deref(m_videopalette); if (m_overtex != NULL) machine().render().texture_free(m_overtex); } //------------------------------------------------- // device reset callback //------------------------------------------------- void laserdisc_device::device_reset() { // attempt to wire up the audio m_stream->set_sample_rate(m_samplerate); // set up the general ld m_audiosquelch = 3; m_videosquelch = 1; m_fieldnum = 0; m_curtrack = 1; m_attospertrack = 0; m_sliderupdate = machine().time(); } //------------------------------------------------- // device_timer - handle timers set by this // device //------------------------------------------------- void laserdisc_device::device_timer(emu_timer &timer, device_timer_id id, int param, void *ptr) { switch (id) { case TID_VBI_FETCH: { // wait for previous read and decode to finish process_track_data(); // update current track based on slider speed update_slider_pos(); // update the state add_and_clamp_track(player_update(m_metadata[m_fieldnum], m_fieldnum, machine().time())); // flush any audio before we read more m_stream->update(); // start reading the track data for the next round m_fieldnum ^= 1; read_track_data(); break; } } } //------------------------------------------------- // sound_stream_update - audio streamer for // laserdiscs //------------------------------------------------- void laserdisc_device::sound_stream_update(sound_stream &stream, stream_sample_t **inputs, stream_sample_t **outputs, int samples) { // compute AND values based on the squelch INT16 leftand = (m_audiosquelch & 1) ? 0x0000 : 0xffff; INT16 rightand = (m_audiosquelch & 2) ? 0x0000 : 0xffff; // see if we have enough samples to fill the buffer; if not, drop out int samples_avail = m_audiobufin - m_audiobufout; if (samples_avail < 0) samples_avail += m_audiobufsize; // if no attached ld, just clear the buffers stream_sample_t *dst0 = outputs[0]; stream_sample_t *dst1 = outputs[1]; if (samples_avail < samples) { memset(dst0, 0, samples * sizeof(dst0[0])); memset(dst1, 0, samples * sizeof(dst1[0])); } // otherwise, stream from our buffer else { INT16 *buffer0 = m_audiobuffer[0]; INT16 *buffer1 = m_audiobuffer[1]; int sampout = m_audiobufout; // copy samples, clearing behind us as we go while (sampout != m_audiobufin && samples-- > 0) { *dst0++ = buffer0[sampout] & leftand; *dst1++ = buffer1[sampout] & rightand; buffer0[sampout] = 0; buffer1[sampout] = 0; sampout++; if (sampout >= m_audiobufsize) sampout = 0; } m_audiobufout = sampout; // clear out the rest of the buffer if (samples > 0) { sampout = (m_audiobufout == 0) ? m_audiobufsize - 1 : m_audiobufout - 1; stream_sample_t fill0 = buffer0[sampout] & leftand; stream_sample_t fill1 = buffer1[sampout] & rightand; while (samples-- > 0) { *dst0++ = fill0; *dst1++ = fill1; } } } } //************************************************************************** // SUBCLASS HELPERS //************************************************************************** //------------------------------------------------- // set_slider_speed - dynamically change the // slider speed //------------------------------------------------- void laserdisc_device::set_slider_speed(INT32 tracks_per_vsync) { // update to the current time update_slider_pos(); // if 0, set the time to 0 attotime vsyncperiod = m_screen->frame_period(); if (tracks_per_vsync == 0) m_attospertrack = 0; // positive values store positive times else if (tracks_per_vsync > 0) m_attospertrack = (vsyncperiod / tracks_per_vsync).as_attoseconds(); // negative values store negative times else m_attospertrack = -(vsyncperiod / -tracks_per_vsync).as_attoseconds(); if (LOG_SLIDER) printf("Slider speed = %d\n", tracks_per_vsync); } //------------------------------------------------- // advance_slider - advance the slider by // a certain number of tracks //------------------------------------------------- void laserdisc_device::advance_slider(INT32 numtracks) { // first update to the current time update_slider_pos(); // then update the track position add_and_clamp_track(numtracks); if (LOG_SLIDER) printf("Advance by %d\n", numtracks); } //------------------------------------------------- // get_slider_position - get the current // slider position //------------------------------------------------- laserdisc_device::slider_position laserdisc_device::get_slider_position() { // update the slider position first update_slider_pos(); // return the status if (m_curtrack == 1) return SLIDER_MINIMUM; else if (m_curtrack < VIRTUAL_LEAD_IN_TRACKS) return SLIDER_VIRTUAL_LEADIN; else if (m_curtrack < VIRTUAL_LEAD_IN_TRACKS + m_chdtracks) return SLIDER_CHD; else if (m_curtrack < VIRTUAL_LEAD_IN_TRACKS + MAX_TOTAL_TRACKS) return SLIDER_OUTSIDE_CHD; else if (m_curtrack < m_maxtrack - 1) return SLIDER_VIRTUAL_LEADOUT; else return SLIDER_MAXIMUM; } //------------------------------------------------- // generic_update - generically update in a way // that works for most situations //------------------------------------------------- INT32 laserdisc_device::generic_update(const vbi_metadata &vbi, int fieldnum, attotime curtime, player_state_info &newstate) { INT32 advanceby = 0; int frame; // start by assuming the state doesn't change newstate = m_player_state; // handle things based on the state switch (m_player_state.m_state) { case LDSTATE_EJECTING: // when time expires, switch to the ejected state if (curtime >= m_player_state.m_endtime) newstate.m_state = LDSTATE_EJECTED; break; case LDSTATE_EJECTED: // do nothing break; case LDSTATE_PARKED: // do nothing break; case LDSTATE_LOADING: // when time expires, switch to the spinup state if (curtime >= m_player_state.m_endtime) newstate.m_state = LDSTATE_SPINUP; advanceby = -GENERIC_SEARCH_SPEED; break; case LDSTATE_SPINUP: // when time expires, switch to the playing state if (curtime >= m_player_state.m_endtime) newstate.m_state = LDSTATE_PLAYING; advanceby = -GENERIC_SEARCH_SPEED; break; case LDSTATE_PAUSING: // if he hit the start of a frame, switch to paused state if (is_start_of_frame(vbi)) { newstate.m_state = LDSTATE_PAUSED; newstate.m_param = fieldnum; } // else advance until we hit it else if (fieldnum == 1) advanceby = 1; break; case LDSTATE_PAUSED: // if we paused on field 1, we must flip back and forth if (m_player_state.m_param == 1) advanceby = (fieldnum == 1) ? 1 : -1; break; case LDSTATE_PLAYING: // if we hit the target frame, switch to the paused state if (m_player_state.m_param > 0 && is_start_of_frame(vbi) && frame_from_metadata(vbi) == m_player_state.m_param) { newstate.m_state = LDSTATE_PAUSED; newstate.m_param = fieldnum; } // otherwise after the second field of each frame else if (fieldnum == 1) advanceby = 1; break; case LDSTATE_PLAYING_SLOW_REVERSE: // after the second field of each frame, see if we need to advance if (fieldnum == 1 && ++m_player_state.m_substate > m_player_state.m_param) { advanceby = -1; m_player_state.m_substate = 0; } break; case LDSTATE_PLAYING_SLOW_FORWARD: // after the second field of each frame, see if we need to advance if (fieldnum == 1 && ++m_player_state.m_substate > m_player_state.m_param) { advanceby = 1; m_player_state.m_substate = 0; } break; case LDSTATE_PLAYING_FAST_REVERSE: // advance after the second field of each frame if (fieldnum == 1) advanceby = -m_player_state.m_param; break; case LDSTATE_PLAYING_FAST_FORWARD: // advance after the second field of each frame if (fieldnum == 1) advanceby = m_player_state.m_param; break; case LDSTATE_SCANNING: // advance after the second field of each frame if (fieldnum == 1) advanceby = m_player_state.m_param >> 8; // after we run out of vsyncs, revert to the saved state if (++m_player_state.m_substate >= (m_player_state.m_param & 0xff)) newstate = m_saved_state; break; case LDSTATE_STEPPING_REVERSE: // wait for the first field of the frame and then leap backwards if (is_start_of_frame(vbi)) { advanceby = (fieldnum == 1) ? -1 : -2; newstate.m_state = LDSTATE_PAUSING; } break; case LDSTATE_STEPPING_FORWARD: // wait for the first field of the frame and then switch to pausing state if (is_start_of_frame(vbi)) newstate.m_state = LDSTATE_PAUSING; break; case LDSTATE_SEEKING: // if we're in the final state, look for a matching frame and pause there frame = frame_from_metadata(vbi); if (m_player_state.m_substate == 1 && is_start_of_frame(vbi) && frame == m_player_state.m_param) { newstate.m_state = LDSTATE_PAUSED; newstate.m_param = fieldnum; } // otherwise, if we got frame data from the VBI, update our seeking logic else if (m_player_state.m_substate == 0 && frame != FRAME_NOT_PRESENT) { INT32 delta = (m_player_state.m_param - 2) - frame; // if we're within a couple of frames, just play until we hit it if (delta >= 0 && delta <= 2) m_player_state.m_substate++; // otherwise, compute the delta assuming 1:1 track to frame; this will correct eventually else { if (delta < 0) delta--; advanceby = delta; advanceby = MIN(advanceby, GENERIC_SEARCH_SPEED); advanceby = MAX(advanceby, -GENERIC_SEARCH_SPEED); } } // otherwise, keep advancing until we know what's up else { if (fieldnum == 1) advanceby = 1; } break; default: // do nothing break; } return advanceby; } //************************************************************************** // INITIALIZATION //************************************************************************** //------------------------------------------------- // init_disc - initialize the state of the // CHD disc //------------------------------------------------- void laserdisc_device::init_disc() { // get a handle to the disc to play if (!m_getdisc_callback.isnull()) m_disc = m_getdisc_callback(*this); else m_disc = get_disk_handle(machine(), tag()); // set default parameters m_width = 720; m_height = 240; m_fps_times_1million = 59940000; m_samplerate = 48000; // get the disc metadata and extract the ld m_chdtracks = 0; m_maxtrack = VIRTUAL_LEAD_IN_TRACKS + MAX_TOTAL_TRACKS + VIRTUAL_LEAD_OUT_TRACKS; if (m_disc != NULL) { // require the A/V codec and nothing else if (m_disc->compression(0) != CHD_CODEC_AVHUFF || m_disc->compression(1) != CHD_CODEC_NONE) throw emu_fatalerror("Laserdisc video must be compressed with the A/V codec!"); // read the metadata astring metadata; chd_error err = m_disc->read_metadata(AV_METADATA_TAG, 0, metadata); if (err != CHDERR_NONE) throw emu_fatalerror("Non-A/V CHD file specified"); // extract the metadata int fps, fpsfrac, interlaced, channels; if (sscanf(metadata, AV_METADATA_FORMAT, &fps, &fpsfrac, &m_width, &m_height, &interlaced, &channels, &m_samplerate) != 7) throw emu_fatalerror("Invalid metadata in CHD file"); else m_fps_times_1million = fps * 1000000 + fpsfrac; // require interlaced video if (!interlaced) throw emu_fatalerror("Laserdisc video must be interlaced!"); // determine the maximum track and allocate a frame buffer UINT32 totalhunks = m_disc->hunk_count(); m_chdtracks = totalhunks / 2; // allocate memory for the precomputed per-frame metadata err = m_disc->read_metadata(AV_LD_METADATA_TAG, 0, m_vbidata); if (err != CHDERR_NONE || m_vbidata.count() != totalhunks * VBI_PACKED_BYTES) throw emu_fatalerror("Precomputed VBI metadata missing or incorrect size"); } m_maxtrack = MAX(m_maxtrack, VIRTUAL_LEAD_IN_TRACKS + VIRTUAL_LEAD_OUT_TRACKS + m_chdtracks); } //------------------------------------------------- // init_video - initialize the state of the // video rendering //------------------------------------------------- void laserdisc_device::init_video() { // register for VBLANK callbacks m_screen->register_vblank_callback(vblank_state_delegate(FUNC(laserdisc_device::vblank_state_changed), this)); // allocate palette for applying brightness/contrast/gamma m_videopalette = palette_alloc(256, 1); if (m_videopalette == NULL) throw emu_fatalerror("Out of memory allocating video palette"); for (int index = 0; index < 256; index++) palette_entry_set_color(m_videopalette, index, MAKE_RGB(index, index, index)); // allocate video frames for (int index = 0; index < ARRAY_LENGTH(m_frame); index++) { // first allocate a YUY16 bitmap at 2x the height frame_data &frame = m_frame[index]; frame.m_bitmap.allocate(m_width, m_height * 2); frame.m_bitmap.set_palette(m_videopalette); fillbitmap_yuy16(frame.m_bitmap, 40, 109, 240); // make a copy of the bitmap that clips out the VBI and horizontal blanking areas frame.m_visbitmap.wrap(&frame.m_bitmap.pix16(44, frame.m_bitmap.width() * 8 / 720), frame.m_bitmap.width() - 2 * frame.m_bitmap.width() * 8 / 720, frame.m_bitmap.height() - 44, frame.m_bitmap.rowpixels()); frame.m_visbitmap.set_palette(m_videopalette); } // allocate an empty frame of the same size m_emptyframe.allocate(m_width, m_height * 2); m_emptyframe.set_palette(m_videopalette); fillbitmap_yuy16(m_emptyframe, 0, 128, 128); // allocate texture for rendering m_videoenable = true; m_videotex = machine().render().texture_alloc(); if (m_videotex == NULL) fatalerror("Out of memory allocating video texture\n"); // allocate overlay m_overenable = overlay_configured(); if (m_overenable) { // bind our handlers m_overupdate_ind16.bind_relative_to(*owner()); m_overupdate_rgb32.bind_relative_to(*owner()); // configure bitmap formats bitmap_format format = !m_overupdate_ind16.isnull() ? BITMAP_FORMAT_IND16 : BITMAP_FORMAT_RGB32; texture_format texformat = !m_overupdate_ind16.isnull() ? TEXFORMAT_PALETTEA16 : TEXFORMAT_ARGB32; // allocate overlay bitmaps for (int index = 0; index < ARRAY_LENGTH(m_overbitmap); index++) { m_overbitmap[index].set_format(format, texformat); m_overbitmap[index].set_palette(machine().palette); m_overbitmap[index].resize(m_overwidth, m_overheight); } // allocate overlay texture m_overtex = machine().render().texture_alloc(); if (m_overtex == NULL) fatalerror("Out of memory allocating overlay texture\n"); } } //------------------------------------------------- // init_audio - initialize the state of the // audio rendering //------------------------------------------------- void laserdisc_device::init_audio() { // allocate a stream m_stream = stream_alloc(0, 2, 48000); // allocate audio buffers m_audiomaxsamples = ((UINT64)m_samplerate * 1000000 + m_fps_times_1million - 1) / m_fps_times_1million; m_audiobufsize = m_audiomaxsamples * 4; m_audiobuffer[0] = auto_alloc_array(machine(), INT16, m_audiobufsize); m_audiobuffer[1] = auto_alloc_array(machine(), INT16, m_audiobufsize); } //************************************************************************** // INTERNAL HELPERS //************************************************************************** //------------------------------------------------- // fillbitmap_yuy16 - fill a YUY16 bitmap with a // given color pattern //------------------------------------------------- void laserdisc_device::fillbitmap_yuy16(bitmap_yuy16 &bitmap, UINT8 yval, UINT8 cr, UINT8 cb) { UINT16 color0 = (yval << 8) | cb; UINT16 color1 = (yval << 8) | cr; // write 32 bits of color (2 pixels at a time) for (int y = 0; y < bitmap.height(); y++) { UINT16 *dest = &bitmap.pix16(y); for (int x = 0; x < bitmap.width() / 2; x++) { *dest++ = color0; *dest++ = color1; } } } //------------------------------------------------- // update_slider_pos - based on the current // speed and elapsed time, update the current // track position //------------------------------------------------- void laserdisc_device::update_slider_pos() { attotime curtime = machine().time(); // if not moving, update to now if (m_attospertrack == 0) m_sliderupdate = curtime; // otherwise, compute the number of tracks covered else { attoseconds_t delta = (curtime - m_sliderupdate).as_attoseconds(); // determine how many tracks we covered and advance if (m_attospertrack >= 0) { INT32 tracks_covered = delta / m_attospertrack; add_and_clamp_track(tracks_covered); if (tracks_covered != 0) m_sliderupdate += attotime(0, tracks_covered * m_attospertrack); } else { INT32 tracks_covered = delta / -m_attospertrack; add_and_clamp_track(-tracks_covered); if (tracks_covered != 0) m_sliderupdate += attotime(0, tracks_covered * -m_attospertrack); } } } //------------------------------------------------- // vblank_state_changed - called on each state // change of the VBLANK signal //------------------------------------------------- void laserdisc_device::vblank_state_changed(screen_device &screen, bool vblank_state) { // update current track based on slider speed update_slider_pos(); // on rising edge, process previously-read frame and inform the player if (vblank_state) { // call the player's VSYNC callback player_vsync(m_metadata[m_fieldnum], m_fieldnum, machine().time()); // set a timer to begin fetching the next frame just before the VBI data would be fetched timer_set(screen.time_until_pos(16*2), TID_VBI_FETCH); } } //------------------------------------------------- // current_frame - return a reference to the // currently visible frame //------------------------------------------------- laserdisc_device::frame_data &laserdisc_device::current_frame() { // determine the most recent live set of frames frame_data *frame = &m_frame[m_videoindex]; if (frame->m_numfields < 2) frame = &m_frame[(m_videoindex + ARRAY_LENGTH(m_frame) - 1) % ARRAY_LENGTH(m_frame)]; return *frame; } //------------------------------------------------- // read_track_data - read and process data for // a particular video track //------------------------------------------------- void laserdisc_device::read_track_data() { // compute the chdhunk number we are going to read INT32 chdtrack = m_curtrack - 1 - VIRTUAL_LEAD_IN_TRACKS; chdtrack = MAX(chdtrack, 0); chdtrack = MIN(chdtrack, m_chdtracks - 1); UINT32 readhunk = chdtrack * 2 + m_fieldnum; // cheat and look up the metadata we are about to retrieve vbi_metadata vbidata = { 0 }; if (m_vbidata.count() != 0) vbi_metadata_unpack(&vbidata, NULL, &m_vbidata[readhunk * VBI_PACKED_BYTES]); // if we're in the lead-in area, force the VBI data to be standard lead-in if (m_curtrack - 1 < VIRTUAL_LEAD_IN_TRACKS) { vbidata.line16 = 0; vbidata.line17 = vbidata.line18 = vbidata.line1718 = VBI_CODE_LEADIN; } //printf("track %5d.%d: %06X %06X %06X\n", m_curtrack, m_fieldnum, vbidata.line16, vbidata.line17, vbidata.line18); // if we're about to read the first field in a frame, advance frame_data *frame = &m_frame[m_videoindex]; if ((vbidata.line1718 & VBI_MASK_CAV_PICTURE) == VBI_CODE_CAV_PICTURE) { if (frame->m_numfields >= 2) m_videoindex = (m_videoindex + 1) % ARRAY_LENGTH(m_frame); frame = &m_frame[m_videoindex]; frame->m_numfields = 0; } // if we're squelched, reset the frame counter if (m_videosquelch) frame->m_numfields = 0; // remember the last field number frame->m_lastfield = m_curtrack * 2 + m_fieldnum; // set the video target information m_avhuff_config.video.wrap(&frame->m_bitmap.pix16(m_fieldnum), frame->m_bitmap.width(), frame->m_bitmap.height() / 2, frame->m_bitmap.rowpixels() * 2); // set the audio target information if (m_audiobufin + m_audiomaxsamples <= m_audiobufsize) { // if we can fit without wrapping, just read the data directly m_avhuff_config.audio[0] = &m_audiobuffer[0][m_audiobufin]; m_avhuff_config.audio[1] = &m_audiobuffer[1][m_audiobufin]; } else { // otherwise, read to the beginning of the buffer m_avhuff_config.audio[0] = &m_audiobuffer[0][0]; m_avhuff_config.audio[1] = &m_audiobuffer[1][0]; } // override if we're not decoding m_avhuff_config.maxsamples = m_audiomaxsamples; m_avhuff_config.actsamples = &m_audiocursamples; m_audiocursamples = 0; // set the VBI data for the new field from our precomputed data if (m_vbidata.count() != 0) { UINT32 vbiframe; vbi_metadata_unpack(&m_metadata[m_fieldnum], &vbiframe, &m_vbidata[readhunk * VBI_PACKED_BYTES]); } // if we're in the lead-in area, force the VBI data to be standard lead-in if (m_curtrack - 1 < VIRTUAL_LEAD_IN_TRACKS) { m_metadata[m_fieldnum].line16 = 0; m_metadata[m_fieldnum].line17 = m_metadata[m_fieldnum].line18 = m_metadata[m_fieldnum].line1718 = VBI_CODE_LEADIN; } // configure the codec and then read m_readresult = CHDERR_FILE_NOT_FOUND; if (m_disc != NULL && !m_videosquelch) { m_readresult = m_disc->codec_configure(CHD_CODEC_AVHUFF, AVHUFF_CODEC_DECOMPRESS_CONFIG, &m_avhuff_config); if (m_readresult == CHDERR_NONE) { m_queued_hunknum = readhunk; m_readresult = CHDERR_OPERATION_PENDING; osd_work_item_queue(m_work_queue, read_async_static, this, WORK_ITEM_FLAG_AUTO_RELEASE); } } } //------------------------------------------------- // read_async_static - work item callback for // asynchronous reads //------------------------------------------------- void *laserdisc_device::read_async_static(void *param, int threadid) { laserdisc_device &ld = *reinterpret_cast(param); ld.m_readresult = ld.m_disc->read_hunk(ld.m_queued_hunknum, NULL); return NULL; } //------------------------------------------------- // process_track_data - process data from a // track after it has been read //------------------------------------------------- void laserdisc_device::process_track_data() { // wait for the async operation to complete if (m_readresult == CHDERR_OPERATION_PENDING) osd_work_queue_wait(m_work_queue, osd_ticks_per_second() * 10); assert(m_readresult != CHDERR_OPERATION_PENDING); // remove the video if we had an error if (m_readresult != CHDERR_NONE) m_avhuff_config.video.reset(); // count the field as read if we are successful if (m_avhuff_config.video.valid()) { m_frame[m_videoindex].m_numfields++; player_overlay(m_avhuff_config.video); } // pass the audio to the callback if (!m_audio_callback.isnull()) m_audio_callback(*this, m_samplerate, m_audiocursamples, m_avhuff_config.audio[0], m_avhuff_config.audio[1]); // shift audio data if we read it into the beginning of the buffer if (m_audiocursamples != 0 && m_audiobufin != 0) for (int chnum = 0; chnum < 2; chnum++) if (m_avhuff_config.audio[chnum] == &m_audiobuffer[chnum][0]) { // move data to the end int samplesleft = m_audiobufsize - m_audiobufin; samplesleft = MIN(samplesleft, m_audiocursamples); memmove(&m_audiobuffer[chnum][m_audiobufin], &m_audiobuffer[chnum][0], samplesleft * 2); // shift data at the beginning if (samplesleft < m_audiocursamples) memmove(&m_audiobuffer[chnum][0], &m_audiobuffer[chnum][samplesleft], (m_audiocursamples - samplesleft) * 2); } // update the input buffer pointer m_audiobufin = (m_audiobufin + m_audiocursamples) % m_audiobufsize; } //************************************************************************** // CONFIG SETTINGS ACCESS //************************************************************************** //------------------------------------------------- // config_load - read and apply data from the // configuration file //------------------------------------------------- void laserdisc_device::config_load(int config_type, xml_data_node *parentnode) { // we only care about game files if (config_type != CONFIG_TYPE_GAME) return; // might not have any data if (parentnode == NULL) return; // iterate over overlay nodes for (xml_data_node *ldnode = xml_get_sibling(parentnode->child, "device"); ldnode != NULL; ldnode = xml_get_sibling(ldnode->next, "device")) { const char *devtag = xml_get_attribute_string(ldnode, "tag", ""); if (strcmp(devtag, tag()) == 0) { // handle the overlay node xml_data_node *overnode = xml_get_sibling(ldnode->child, "overlay"); if (overnode != NULL) { // fetch positioning controls m_overposx = xml_get_attribute_float(overnode, "hoffset", m_overposx); m_overscalex = xml_get_attribute_float(overnode, "hstretch", m_overscalex); m_overposy = xml_get_attribute_float(overnode, "voffset", m_overposy); m_overscaley = xml_get_attribute_float(overnode, "vstretch", m_overscaley); } } } } //------------------------------------------------- // config_save - save data to the configuration // file //------------------------------------------------- void laserdisc_device::config_save(int config_type, xml_data_node *parentnode) { // we only care about game files if (config_type != CONFIG_TYPE_GAME) return; // create a node xml_data_node *ldnode = xml_add_child(parentnode, "device", NULL); if (ldnode != NULL) { // output the basics xml_set_attribute(ldnode, "tag", tag()); // add an overlay node xml_data_node *overnode = xml_add_child(ldnode, "overlay", NULL); bool changed = false; if (overnode != NULL) { // output the positioning controls if (m_overposx != m_orig_config.m_overposx) { xml_set_attribute_float(overnode, "hoffset", m_overposx); changed = true; } if (m_overscalex != m_orig_config.m_overscalex) { xml_set_attribute_float(overnode, "hstretch", m_overscalex); changed = true; } if (m_overposy != m_orig_config.m_overposy) { xml_set_attribute_float(overnode, "voffset", m_overposy); changed = true; } if (m_overscaley != m_orig_config.m_overscaley) { xml_set_attribute_float(overnode, "vstretch", m_overscaley); changed = true; } } // if nothing changed, kill the node if (!changed) xml_delete_node(ldnode); } }