X-Git-Url: https://git.carlh.net/gitweb/?a=blobdiff_plain;f=libs%2Fardour%2Fmidi_diskstream.cc;h=f960c170db095fbdbf1c1284ced71b7846613b2c;hb=41ac4afff50b1730e61e302e60f66e6ef94720c1;hp=fdf90923084508454ea2afb35b2df1f138e85b03;hpb=22c20ab6f215c0ab24702a479aa6821c25a7d058;p=ardour.git diff --git a/libs/ardour/midi_diskstream.cc b/libs/ardour/midi_diskstream.cc index fdf9092308..f960c170db 100644 --- a/libs/ardour/midi_diskstream.cc +++ b/libs/ardour/midi_diskstream.cc @@ -14,8 +14,6 @@ You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. - - $Id: diskstream.cc 567 2006-06-07 14:54:12Z trutkin $ */ #include @@ -35,18 +33,24 @@ #include #include #include +#include +#include #include #include -#include -#include #include -#include -#include -#include -#include #include +#include +#include +#include +#include #include +#include +#include +#include +#include +#include +#include #include "i18n.h" #include @@ -55,27 +59,39 @@ using namespace std; using namespace ARDOUR; using namespace PBD; -sigc::signal*> MidiDiskstream::DeleteSources; +nframes_t MidiDiskstream::midi_readahead = 4096; MidiDiskstream::MidiDiskstream (Session &sess, const string &name, Diskstream::Flag flag) : Diskstream(sess, name, flag) - , _playlist(NULL) + , _playback_buf(0) + , _capture_buf(0) + , _source_port(0) + , _last_flush_frame(0) + , _note_mode(Sustained) + , _frames_written_to_ringbuffer(0) + , _frames_read_from_ringbuffer(0) { /* prevent any write sources from being created */ in_set_state = true; - init (flag); + init(flag); use_new_playlist (); in_set_state = false; - DiskstreamCreated (this); /* EMIT SIGNAL */ + assert(!destructive()); } MidiDiskstream::MidiDiskstream (Session& sess, const XMLNode& node) : Diskstream(sess, node) - , _playlist(NULL) + , _playback_buf(0) + , _capture_buf(0) + , _source_port(0) + , _last_flush_frame(0) + , _note_mode(Sustained) + , _frames_written_to_ringbuffer(0) + , _frames_read_from_ringbuffer(0) { in_set_state = true; init (Recordable); @@ -90,8 +106,6 @@ MidiDiskstream::MidiDiskstream (Session& sess, const XMLNode& node) if (destructive()) { use_destructive_playlist (); } - - DiskstreamCreated (this); /* EMIT SIGNAL */ } void @@ -107,57 +121,103 @@ MidiDiskstream::init (Diskstream::Flag f) set_block_size (_session.get_block_size()); allocate_temporary_buffers (); - /* FIXME: this is now done before the above. OK? */ - /*pending_overwrite = false; - overwrite_frame = 0; - overwrite_queued = false; - input_change_pending = NoChange;*/ + const size_t size = _session.midi_diskstream_buffer_size(); + _playback_buf = new MidiRingBuffer(size); + _capture_buf = new MidiRingBuffer(size); + + _n_channels = ChanCount(DataType::MIDI, 1); - _n_channels = 1; + assert(recordable()); } MidiDiskstream::~MidiDiskstream () { Glib::Mutex::Lock lm (state_lock); - - if (_playlist) - _playlist->unref (); } -/* + + void -MidiDiskstream::handle_input_change (IOChange change, void *src) +MidiDiskstream::non_realtime_locate (nframes_t position) { - Glib::Mutex::Lock lm (state_lock); - - if (!(input_change_pending & change)) { - input_change_pending = IOChange (input_change_pending|change); - _session.request_input_change_handling (); - } + assert(_write_source); + _write_source->set_timeline_position (position); + seek(position, false); } -*/ + + void MidiDiskstream::non_realtime_input_change () { + { + Glib::Mutex::Lock lm (state_lock); + + if (input_change_pending == NoChange) { + return; + } + + if (input_change_pending & ConfigurationChanged) { + assert(_io->n_inputs() == _n_channels); + } + + get_input_sources (); + set_capture_offset (); + + if (first_input_change) { + set_align_style (_persistent_alignment_style); + first_input_change = false; + } else { + set_align_style_from_io (); + } + + input_change_pending = NoChange; + + /* implicit unlock */ + } + + /* reset capture files */ + + reset_write_sources (false); + + /* now refill channel buffers */ + + if (speed() != 1.0f || speed() != -1.0f) { + seek ((nframes_t) (_session.transport_frame() * (double) speed())); + } + else { + seek (_session.transport_frame()); + } + + _last_flush_frame = _session.transport_frame(); } void MidiDiskstream::get_input_sources () { + uint32_t ni = _io->n_inputs().n_midi(); + + if (ni == 0) { + return; + } + + // This is all we do for now at least + assert(ni == 1); + + _source_port = _io->midi_input(0); + + // do... stuff? } int MidiDiskstream::find_and_use_playlist (const string& name) { - Playlist* pl; - MidiPlaylist* playlist; + boost::shared_ptr playlist; - if ((pl = _session.playlist_by_name (name)) == 0) { - playlist = new MidiPlaylist(_session, name); - pl = playlist; + if ((playlist = boost::dynamic_pointer_cast (_session.playlist_by_name (name))) == 0) { + playlist = boost::dynamic_pointer_cast (PlaylistFactory::create (DataType::MIDI, _session, name)); } - if ((playlist = dynamic_cast (pl)) == 0) { - error << string_compose(_("MidiDiskstream: Playlist \"%1\" isn't a midi playlist"), name) << endmsg; + if (!playlist) { + error << string_compose(_("MidiDiskstream: Playlist \"%1\" isn't an midi playlist"), name) << endmsg; return -1; } @@ -165,53 +225,20 @@ MidiDiskstream::find_and_use_playlist (const string& name) } int -MidiDiskstream::use_playlist (Playlist* playlist) -{ - assert(dynamic_cast(playlist)); - - { - Glib::Mutex::Lock lm (state_lock); - - if (playlist == _playlist) { - return 0; - } +MidiDiskstream::use_playlist (boost::shared_ptr playlist) +{ + assert(boost::dynamic_pointer_cast(playlist)); - plstate_connection.disconnect(); - plmod_connection.disconnect (); - plgone_connection.disconnect (); - - if (_playlist) { - _playlist->unref(); - } - - _playlist = dynamic_cast(playlist); - _playlist->ref(); - - if (!in_set_state && recordable()) { - reset_write_sources (false); - } - - plstate_connection = _playlist->StateChanged.connect (mem_fun (*this, &MidiDiskstream::playlist_changed)); - plmod_connection = _playlist->Modified.connect (mem_fun (*this, &MidiDiskstream::playlist_modified)); - plgone_connection = _playlist->GoingAway.connect (mem_fun (*this, &MidiDiskstream::playlist_deleted)); - } - - if (!overwrite_queued) { - _session.request_overwrite_buffer (this); - overwrite_queued = true; - } - - PlaylistChanged (); /* EMIT SIGNAL */ - _session.set_dirty (); + Diskstream::use_playlist(playlist); return 0; } int MidiDiskstream::use_new_playlist () -{ +{ string newname; - MidiPlaylist* playlist; + boost::shared_ptr playlist; if (!in_set_state && destructive()) { return 0; @@ -223,9 +250,12 @@ MidiDiskstream::use_new_playlist () newname = Playlist::bump_name (_name, _session); } - if ((playlist = new MidiPlaylist (_session, newname, hidden())) != 0) { + if ((playlist = boost::dynamic_pointer_cast (PlaylistFactory::create ( + DataType::MIDI, _session, newname, hidden()))) != 0) { + playlist->set_orig_diskstream_id (id()); return use_playlist (playlist); + } else { return -1; } @@ -234,6 +264,8 @@ MidiDiskstream::use_new_playlist () int MidiDiskstream::use_copy_playlist () { + assert(midi_playlist()); + if (destructive()) { return 0; } @@ -244,11 +276,11 @@ MidiDiskstream::use_copy_playlist () } string newname; - MidiPlaylist* playlist; + boost::shared_ptr playlist; newname = Playlist::bump_name (_playlist->name(), _session); - if ((playlist = new MidiPlaylist (*_playlist, newname)) != 0) { + if ((playlist = boost::dynamic_pointer_cast(PlaylistFactory::create (midi_playlist(), newname))) != 0) { playlist->set_orig_diskstream_id (id()); return use_playlist (playlist); } else { @@ -256,96 +288,355 @@ MidiDiskstream::use_copy_playlist () } } +/** Overloaded from parent to die horribly + */ +int +MidiDiskstream::set_destructive (bool yn) +{ + assert( ! destructive()); + assert( ! yn); + return -1; +} + +void +MidiDiskstream::set_note_mode (NoteMode m) +{ + _note_mode = m; + midi_playlist()->set_note_mode(m); + if (_write_source && _write_source->model()) + _write_source->model()->set_note_mode(m); +} void -MidiDiskstream::playlist_deleted (Playlist* pl) +MidiDiskstream::check_record_status (nframes_t transport_frame, nframes_t nframes, bool can_record) { - /* this catches an ordering issue with session destruction. playlists - are destroyed before diskstreams. we have to invalidate any handles - we have to the playlist. + // FIXME: waaay too much code to duplicate (AudioDiskstream) + + int possibly_recording; + int rolling; + int change; + const int transport_rolling = 0x4; + const int track_rec_enabled = 0x2; + const int global_rec_enabled = 0x1; + + /* merge together the 3 factors that affect record status, and compute + what has changed. */ - _playlist = 0; -} + rolling = _session.transport_speed() != 0.0f; + possibly_recording = (rolling << 2) | (record_enabled() << 1) | can_record; + change = possibly_recording ^ last_possibly_recording; + if (possibly_recording == last_possibly_recording) { + return; + } -void -MidiDiskstream::setup_destructive_playlist () -{ - /* a single full-sized region */ + /* change state */ + + /* if per-track or global rec-enable turned on while the other was already on, we've started recording */ + + if (((change & track_rec_enabled) && record_enabled() && (!(change & global_rec_enabled) && can_record)) || + ((change & global_rec_enabled) && can_record && (!(change & track_rec_enabled) && record_enabled()))) { + + /* starting to record: compute first+last frames */ + + first_recordable_frame = transport_frame + _capture_offset; + last_recordable_frame = max_frames; + capture_start_frame = transport_frame; + + if (!(last_possibly_recording & transport_rolling) && (possibly_recording & transport_rolling)) { + + /* was stopped, now rolling (and recording) */ + + if (_alignment_style == ExistingMaterial) { + first_recordable_frame += _session.worst_output_latency(); + } else { + first_recordable_frame += _roll_delay; + } + + } else { + + /* was rolling, but record state changed */ + + if (_alignment_style == ExistingMaterial) { + + + if (!Config->get_punch_in()) { + + /* manual punch in happens at the correct transport frame + because the user hit a button. but to get alignment correct + we have to back up the position of the new region to the + appropriate spot given the roll delay. + */ + + capture_start_frame -= _roll_delay; + + /* XXX paul notes (august 2005): i don't know why + this is needed. + */ + + first_recordable_frame += _capture_offset; + + } else { + + /* autopunch toggles recording at the precise + transport frame, and then the DS waits + to start recording for a time that depends + on the output latency. + */ + + first_recordable_frame += _session.worst_output_latency(); + } + + } else { + + if (Config->get_punch_in()) { + first_recordable_frame += _roll_delay; + } else { + capture_start_frame -= _roll_delay; + } + } + + } + + } else if (!record_enabled() || !can_record) { + + /* stop recording */ - //MidiRegion* region = new MidiRegion (srcs, 0, max_frames, _name); - //_playlist->add_region (*region, 0); + last_recordable_frame = transport_frame + _capture_offset; + + if (_alignment_style == ExistingMaterial) { + last_recordable_frame += _session.worst_output_latency(); + } else { + last_recordable_frame += _roll_delay; + } + } + + last_possibly_recording = possibly_recording; } -void -MidiDiskstream::use_destructive_playlist () +int +MidiDiskstream::process (nframes_t transport_frame, nframes_t nframes, nframes_t offset, bool can_record, bool rec_monitors_input) { - /* use the sources associated with the single full-extent region */ + // FIXME: waay too much code to duplicate (AudioDiskstream::process) + int ret = -1; + nframes_t rec_offset = 0; + nframes_t rec_nframes = 0; + bool nominally_recording; + bool re = record_enabled (); + bool collect_playback = false; + + /* if we've already processed the frames corresponding to this call, + just return. this allows multiple routes that are taking input + from this diskstream to call our ::process() method, but have + this stuff only happen once. more commonly, it allows both + the AudioTrack that is using this AudioDiskstream *and* the Session + to call process() without problems. + */ + + if (_processed) { + return 0; + } - Playlist::RegionList* rl = _playlist->regions_at (0); + commit_should_unlock = false; - if (rl->empty()) { - reset_write_sources (false, true); - return; + check_record_status (transport_frame, nframes, can_record); + + nominally_recording = (can_record && re); + + if (nframes == 0) { + _processed = true; + return 0; } - MidiRegion* region = dynamic_cast (rl->front()); + /* This lock is held until the end of ::commit, so these two functions + must always be called as a pair. The only exception is if this function + returns a non-zero value, in which case, ::commit should not be called. + */ - if (region == 0) { - throw failed_constructor(); + // If we can't take the state lock return. + if (!state_lock.trylock()) { + return 1; } + commit_should_unlock = true; + adjust_capture_position = 0; + + if (nominally_recording || (_session.get_record_enabled() && Config->get_punch_in())) { + OverlapType ot; + + ot = coverage (first_recordable_frame, last_recordable_frame, transport_frame, transport_frame + nframes); + + switch (ot) { + case OverlapNone: + rec_nframes = 0; + break; + + case OverlapInternal: + /* ---------- recrange + |---| transrange + */ + rec_nframes = nframes; + rec_offset = 0; + break; + + case OverlapStart: + /* |--------| recrange + -----| transrange + */ + rec_nframes = transport_frame + nframes - first_recordable_frame; + if (rec_nframes) { + rec_offset = first_recordable_frame - transport_frame; + } + break; + + case OverlapEnd: + /* |--------| recrange + |-------- transrange + */ + rec_nframes = last_recordable_frame - transport_frame; + rec_offset = 0; + break; + + case OverlapExternal: + /* |--------| recrange + -------------- transrange + */ + rec_nframes = last_recordable_frame - last_recordable_frame; + rec_offset = first_recordable_frame - transport_frame; + break; + } - delete rl; + if (rec_nframes && !was_recording) { + capture_captured = 0; + was_recording = true; + } + } - /* the source list will never be reset for a destructive track */ -} -void -MidiDiskstream::set_io (IO& io) -{ - _io = &io; - set_align_style_from_io (); -} + if (can_record && !_last_capture_regions.empty()) { + _last_capture_regions.clear (); + } -void -MidiDiskstream::non_realtime_set_speed () -{ - if (_buffer_reallocation_required) - { - Glib::Mutex::Lock lm (state_lock); - allocate_temporary_buffers (); + if (nominally_recording || rec_nframes) { + + // Pump entire port buffer into the ring buffer (FIXME: split cycles?) + MidiBuffer& buf = _source_port->get_midi_buffer(nframes, offset); + for (MidiBuffer::iterator i = buf.begin(); i != buf.end(); ++i) { + const Evoral::MIDIEvent ev(*i, false); + assert(ev.buffer()); + _capture_buf->write(ev.time() + transport_frame, ev.type(), ev.size(), ev.buffer()); + } + + } else { + + if (was_recording) { + finish_capture (rec_monitors_input); + } - _buffer_reallocation_required = false; } - if (_seek_required) { - if (speed() != 1.0f || speed() != -1.0f) { - seek ((jack_nframes_t) (_session.transport_frame() * (double) speed()), true); + if (rec_nframes) { + + /* XXX XXX XXX XXX XXX XXX XXX XXX */ + + /* data will be written to disk */ + + if (rec_nframes == nframes && rec_offset == 0) { + + playback_distance = nframes; + } else { + + collect_playback = true; } - else { - seek (_session.transport_frame(), true); + + adjust_capture_position = rec_nframes; + + } else if (nominally_recording) { + + /* can't do actual capture yet - waiting for latency effects to finish before we start*/ + + playback_distance = nframes; + + } else { + + collect_playback = true; + } + + if (collect_playback) { + + /* we're doing playback */ + + nframes_t necessary_samples; + + /* no varispeed playback if we're recording, because the output .... TBD */ + + if (rec_nframes == 0 && _actual_speed != 1.0f) { + necessary_samples = (nframes_t) floor ((nframes * fabs (_actual_speed))) + 1; + } else { + necessary_samples = nframes; } - _seek_required = false; + // XXX XXX XXX XXX XXX XXX XXX XXX XXX XXX + // Write into playback buffer here, and whatnot? + //cerr << "MDS FIXME: collect playback" << endl; + } -} -void -MidiDiskstream::check_record_status (jack_nframes_t transport_frame, jack_nframes_t nframes, bool can_record) -{ -} + ret = 0; -int -MidiDiskstream::process (jack_nframes_t transport_frame, jack_nframes_t nframes, jack_nframes_t offset, bool can_record, bool rec_monitors_input) -{ - return 0; + _processed = true; + + if (ret) { + + /* we're exiting with failure, so ::commit will not + be called. unlock the state lock. + */ + + commit_should_unlock = false; + state_lock.unlock(); + } + + return ret; } bool -MidiDiskstream::commit (jack_nframes_t nframes) +MidiDiskstream::commit (nframes_t nframes) { - return 0; + bool need_butler = false; + + if (_actual_speed < 0.0) { + playback_sample -= playback_distance; + } else { + playback_sample += playback_distance; + } + + if (adjust_capture_position != 0) { + capture_captured += adjust_capture_position; + adjust_capture_position = 0; + } + + /* what audio does: + * can't do this with midi: write space is in bytes, chunk_frames is in frames + if (_slaved) { + need_butler = _playback_buf->write_space() >= _playback_buf->capacity() / 2; + } else { + need_butler = _playback_buf->write_space() >= disk_io_chunk_frames + || _capture_buf->read_space() >= disk_io_chunk_frames; + }*/ + + // Use The Counters To calculate how much time the Ringbuffer holds. + uint32_t frames_read = g_atomic_int_get(&_frames_read_from_ringbuffer); + uint32_t frames_written = g_atomic_int_get(&_frames_written_to_ringbuffer); + if ((frames_written - frames_read) <= midi_readahead) + need_butler = true; + + if (commit_should_unlock) { + state_lock.unlock(); + } + + _processed = false; + + return need_butler; } void @@ -354,66 +645,544 @@ MidiDiskstream::set_pending_overwrite (bool yn) /* called from audio thread, so we can use the read ptr and playback sample as we wish */ pending_overwrite = yn; - + overwrite_frame = playback_sample; - //overwrite_offset = channels.front().playback_buf->get_read_ptr(); } int MidiDiskstream::overwrite_existing_buffers () { + //read(overwrite_frame, disk_io_chunk_frames, false); + overwrite_queued = false; + pending_overwrite = false; + return 0; } int -MidiDiskstream::seek (jack_nframes_t frame, bool complete_refill) +MidiDiskstream::seek (nframes_t frame, bool complete_refill) { - return 0; + Glib::Mutex::Lock lm (state_lock); + int ret = -1; + + _playback_buf->reset(); + _capture_buf->reset(); + g_atomic_int_set(&_frames_read_from_ringbuffer, 0); + g_atomic_int_set(&_frames_written_to_ringbuffer, 0); + + playback_sample = frame; + file_frame = frame; + + if (complete_refill) { + while ((ret = do_refill_with_alloc ()) > 0) ; + } else { + ret = do_refill_with_alloc (); + } + + return ret; } int -MidiDiskstream::can_internal_playback_seek (jack_nframes_t distance) +MidiDiskstream::can_internal_playback_seek (nframes_t distance) { - return 0; + uint32_t frames_read = g_atomic_int_get(&_frames_read_from_ringbuffer); + uint32_t frames_written = g_atomic_int_get(&_frames_written_to_ringbuffer); + if ((frames_written-frames_read) < distance) { + return false; + } else { + return true; + } } int -MidiDiskstream::internal_playback_seek (jack_nframes_t distance) +MidiDiskstream::internal_playback_seek (nframes_t distance) { + cerr << "MDS: internal_playback_seek " << distance << endl; + + first_recordable_frame += distance; + playback_sample += distance; + return 0; } +/** @a start is set to the new frame position (TIME) read up to */ int -MidiDiskstream::read (RawMidi* buf, RawMidi* mixdown_buffer, char * workbuf, jack_nframes_t& start, jack_nframes_t cnt, bool reversed) -{ +MidiDiskstream::read (nframes_t& start, nframes_t dur, bool reversed) +{ + nframes_t this_read = 0; + bool reloop = false; + nframes_t loop_end = 0; + nframes_t loop_start = 0; + nframes_t loop_length = 0; + Location *loc = 0; + + if (!reversed) { + /* Make the use of a Location atomic for this read operation. + + Note: Locations don't get deleted, so all we care about + when I say "atomic" is that we are always pointing to + the same one and using a start/length values obtained + just once. + */ + + if ((loc = loop_location) != 0) { + loop_start = loc->start(); + loop_end = loc->end(); + loop_length = loop_end - loop_start; + } + + /* if we are looping, ensure that the first frame we read is at the correct + position within the loop. + */ + + if (loc && (start >= loop_end)) { + //cerr << "start adjusted from " << start; + start = loop_start + ((start - loop_start) % loop_length); + //cerr << "to " << start << endl; + } + //cerr << "start is " << start << " loopstart: " << loop_start << " loopend: " << loop_end << endl; + } + + while (dur) { + + /* take any loop into account. we can't read past the end of the loop. */ + + if (loc && (loop_end - start < dur)) { + this_read = loop_end - start; + //cerr << "reloop true: thisread: " << this_read << " dur: " << dur << endl; + reloop = true; + } else { + reloop = false; + this_read = dur; + } + + if (this_read == 0) { + break; + } + + this_read = min(dur,this_read); + + if (midi_playlist()->read (*_playback_buf, start, this_read) != this_read) { + error << string_compose(_("MidiDiskstream %1: cannot read %2 from playlist at frame %3"), _id, this_read, + start) << endmsg; + return -1; + } + //cout << "this write " << this_read << "start= " << start << endl; + g_atomic_int_add(&_frames_written_to_ringbuffer, this_read); + + _read_data_count = _playlist->read_data_count(); + + if (reversed) { + + // Swap note ons with note offs here. etc? + // Fully reversing MIDI required look-ahead (well, behind) to find previous + // CC values etc. hard. + + } else { + + /* if we read to the end of the loop, go back to the beginning */ + + if (reloop) { + // Synthesize LoopEvent here, because the next events + // written will have non-monotonic timestamps. + _playback_buf->write(loop_end - 1, LoopEventType, 0, 0); + //cout << "Pushing LoopEvent ts=" << loop_end-1 + // << " start+this_read " << start+this_read << endl; + + start = loop_start; + } else { + start += this_read; + } + } + + dur -= this_read; + //offset += this_read; + } + return 0; } int -MidiDiskstream::do_refill (RawMidi* mixdown_buffer, float* gain_buffer, char * workbuf) +MidiDiskstream::do_refill_with_alloc () { - return 0; -} + return do_refill(); +} int -MidiDiskstream::do_flush (char * workbuf, bool force_flush) +MidiDiskstream::do_refill () { - return 0; + int ret = 0; + size_t write_space = _playback_buf->write_space(); + bool reversed = (_visible_speed * _session.transport_speed()) < 0.0f; + + if (write_space == 0) { + return 0; + } + + if (reversed) { + return 0; + } + + /* at end: nothing to do */ + if (file_frame == max_frames) { + return 0; + } + + // At this point we... + assert(_playback_buf->write_space() > 0); // ... have something to write to, and + assert(file_frame <= max_frames); // ... something to write + + // now calculate how much time is in the ringbuffer. + // and lets write as much as we need to get this to be midi_readahead; + uint32_t frames_read = g_atomic_int_get(&_frames_read_from_ringbuffer); + uint32_t frames_written = g_atomic_int_get(&_frames_written_to_ringbuffer); + if ((frames_written-frames_read) >= midi_readahead) { + //cout << "Nothing to do. all fine" << endl; + return 0; + } + + nframes_t to_read = midi_readahead - (frames_written - frames_read); + + //cout << "read for midi_readahead " << to_read << " rb_contains: " << frames_written-frames_read << endl; + + to_read = min(to_read, (max_frames - file_frame)); + + if (read (file_frame, to_read, reversed)) { + ret = -1; + } + + return ret; +} + +/** Flush pending data to disk. + * + * Important note: this function will write *AT MOST* disk_io_chunk_frames + * of data to disk. it will never write more than that. If it writes that + * much and there is more than that waiting to be written, it will return 1, + * otherwise 0 on success or -1 on failure. + * + * If there is less than disk_io_chunk_frames to be written, no data will be + * written at all unless @a force_flush is true. + */ +int +MidiDiskstream::do_flush (RunContext context, bool force_flush) +{ + uint32_t to_write; + int32_t ret = 0; + nframes_t total; + + _write_data_count = 0; + + if (_last_flush_frame > _session.transport_frame() + || _last_flush_frame < capture_start_frame) { + _last_flush_frame = _session.transport_frame(); + } + + total = _session.transport_frame() - _last_flush_frame; + + if (total == 0 || (_capture_buf->read_space() == 0 && _session.transport_speed() == 0) || (total < disk_io_chunk_frames && !force_flush && was_recording)) { + goto out; + } + + /* if there are 2+ chunks of disk i/o possible for + this track, let the caller know so that it can arrange + for us to be called again, ASAP. + + if we are forcing a flush, then if there is* any* extra + work, let the caller know. + + if we are no longer recording and there is any extra work, + let the caller know too. + */ + + if (total >= 2 * disk_io_chunk_frames || ((force_flush || !was_recording) && total > disk_io_chunk_frames)) { + ret = 1; + } + + to_write = disk_io_chunk_frames; + + assert(!destructive()); + + if (record_enabled() && _session.transport_frame() - _last_flush_frame > disk_io_chunk_frames) { + if ((!_write_source) || _write_source->midi_write (*_capture_buf, to_write) != to_write) { + error << string_compose(_("MidiDiskstream %1: cannot write to disk"), _id) << endmsg; + return -1; + } else { + _last_flush_frame = _session.transport_frame(); + } + } + +out: + return ret; } void MidiDiskstream::transport_stopped (struct tm& when, time_t twhen, bool abort_capture) { + uint32_t buffer_position; + bool more_work = true; + int err = 0; + boost::shared_ptr region; + nframes_t total_capture; + MidiRegion::SourceList srcs; + MidiRegion::SourceList::iterator src; + vector::iterator ci; + bool mark_write_completed = false; + + finish_capture (true); + + /* butler is already stopped, but there may be work to do + to flush remaining data to disk. + */ + + while (more_work && !err) { + switch (do_flush (TransportContext, true)) { + case 0: + more_work = false; + break; + case 1: + break; + case -1: + error << string_compose(_("MidiDiskstream \"%1\": cannot flush captured data to disk!"), _name) << endmsg; + err++; + } + } + + /* XXX is there anything we can do if err != 0 ? */ + Glib::Mutex::Lock lm (capture_info_lock); + + if (capture_info.empty()) { + return; + } + + if (abort_capture) { + + if (_write_source) { + + _write_source->mark_for_remove (); + _write_source->drop_references (); + _write_source.reset(); + } + + /* new source set up in "out" below */ + + } else { + + assert(_write_source); + + for (total_capture = 0, ci = capture_info.begin(); ci != capture_info.end(); ++ci) { + total_capture += (*ci)->frames; + } + + /* figure out the name for this take */ + + srcs.push_back (_write_source); + _write_source->set_timeline_position (capture_info.front()->start); + _write_source->set_captured_for (_name); + + string whole_file_region_name; + whole_file_region_name = region_name_from_path (_write_source->name(), true); + + /* Register a new region with the Session that + describes the entire source. Do this first + so that any sub-regions will obviously be + children of this one (later!) + */ + + try { + boost::shared_ptr rx (RegionFactory::create (srcs, 0, + total_capture, whole_file_region_name, 0, + Region::Flag (Region::DefaultFlags|Region::Automatic|Region::WholeFile))); + + region = boost::dynamic_pointer_cast (rx); + region->special_set_position (capture_info.front()->start); + } + + + catch (failed_constructor& err) { + error << string_compose(_("%1: could not create region for complete midi file"), _name) << endmsg; + /* XXX what now? */ + } + + _last_capture_regions.push_back (region); + + // cerr << _name << ": there are " << capture_info.size() << " capture_info records\n"; + + XMLNode &before = _playlist->get_state(); + _playlist->freeze (); + + for (buffer_position = 0, ci = capture_info.begin(); ci != capture_info.end(); ++ci) { + + string region_name; + + _session.region_name (region_name, _write_source->name(), false); + + // cerr << _name << ": based on ci of " << (*ci)->start << " for " << (*ci)->frames << " add a region\n"; + + try { + boost::shared_ptr rx (RegionFactory::create (srcs, buffer_position, (*ci)->frames, region_name)); + region = boost::dynamic_pointer_cast (rx); + } + + catch (failed_constructor& err) { + error << _("MidiDiskstream: could not create region for captured midi!") << endmsg; + continue; /* XXX is this OK? */ + } + + region->GoingAway.connect (bind (mem_fun (*this, &Diskstream::remove_region_from_last_capture), boost::weak_ptr(region))); + + _last_capture_regions.push_back (region); + + // cerr << "add new region, buffer position = " << buffer_position << " @ " << (*ci)->start << endl; + + i_am_the_modifier++; + _playlist->add_region (region, (*ci)->start); + i_am_the_modifier--; + + buffer_position += (*ci)->frames; + } + + _playlist->thaw (); + XMLNode &after = _playlist->get_state(); + _session.add_command (new MementoCommand(*_playlist, &before, &after)); + + } + + mark_write_completed = true; + + reset_write_sources (mark_write_completed); + + for (ci = capture_info.begin(); ci != capture_info.end(); ++ci) { + delete *ci; + } + + capture_info.clear (); + capture_start_frame = 0; +} + +void +MidiDiskstream::transport_looped (nframes_t transport_frame) +{ + if (was_recording) { + + // adjust the capture length knowing that the data will be recorded to disk + // only necessary after the first loop where we're recording + if (capture_info.size() == 0) { + capture_captured += _capture_offset; + + if (_alignment_style == ExistingMaterial) { + capture_captured += _session.worst_output_latency(); + } else { + capture_captured += _roll_delay; + } + } + + finish_capture (true); + + // the next region will start recording via the normal mechanism + // we'll set the start position to the current transport pos + // no latency adjustment or capture offset needs to be made, as that already happened the first time + capture_start_frame = transport_frame; + first_recordable_frame = transport_frame; // mild lie + last_recordable_frame = max_frames; + was_recording = true; + } } void MidiDiskstream::finish_capture (bool rec_monitors_input) { + was_recording = false; + + if (capture_captured == 0) { + return; + } + + // Why must we destroy? + assert(!destructive()); + + CaptureInfo* ci = new CaptureInfo; + + ci->start = capture_start_frame; + ci->frames = capture_captured; + + /* XXX theoretical race condition here. Need atomic exchange ? + However, the circumstances when this is called right + now (either on record-disable or transport_stopped) + mean that no actual race exists. I think ... + We now have a capture_info_lock, but it is only to be used + to synchronize in the transport_stop and the capture info + accessors, so that invalidation will not occur (both non-realtime). + */ + + // cerr << "Finish capture, add new CI, " << ci->start << '+' << ci->frames << endl; + + capture_info.push_back (ci); + capture_captured = 0; } void -MidiDiskstream::set_record_enabled (bool yn, void* src) +MidiDiskstream::set_record_enabled (bool yn) { + if (!recordable() || !_session.record_enabling_legal()) { + return; + } + + assert(!destructive()); + + if (yn && _source_port == 0) { + + /* pick up connections not initiated *from* the IO object + we're associated with. + */ + + get_input_sources (); + } + + /* yes, i know that this not proof against race conditions, but its + good enough. i think. + */ + + if (record_enabled() != yn) { + if (yn) { + engage_record_enable (); + } else { + disengage_record_enable (); + } + } +} + +void +MidiDiskstream::engage_record_enable () +{ + bool rolling = _session.transport_speed() != 0.0f; + + g_atomic_int_set (&_record_enabled, 1); + + if (_source_port && Config->get_monitoring_model() == HardwareMonitoring) { + _source_port->request_monitor_input (!(Config->get_auto_input() && rolling)); + } + + // FIXME: Why is this necessary? Isn't needed for AudioDiskstream... + if (!_write_source) + use_new_write_source(); + + _write_source->mark_streaming_midi_write_started (_note_mode, _session.transport_frame()); + + RecordEnableChanged (); /* EMIT SIGNAL */ +} + +void +MidiDiskstream::disengage_record_enable () +{ + g_atomic_int_set (&_record_enabled, 0); + if (_source_port && Config->get_monitoring_model() == HardwareMonitoring) { + if (_source_port) { + _source_port->request_monitor_input (false); + } + } + + RecordEnableChanged (); /* EMIT SIGNAL */ } XMLNode& @@ -426,31 +1195,34 @@ MidiDiskstream::get_state () snprintf (buf, sizeof(buf), "0x%x", _flags); node->add_property ("flags", buf); + node->add_property("channel-mode", enum_2_string(get_channel_mode())); + + snprintf (buf, sizeof(buf), "0x%x", get_channel_mask()); + node->add_property("channel-mask", buf); + node->add_property ("playlist", _playlist->name()); snprintf (buf, sizeof(buf), "%f", _visible_speed); node->add_property ("speed", buf); node->add_property("name", _name); - snprintf (buf, sizeof(buf), "%" PRIu64, id()); + id().print(buf, sizeof(buf)); node->add_property("id", buf); - if (!_capturing_sources.empty() && _session.get_record_enabled()) { + if (_write_source && _session.get_record_enabled()) { XMLNode* cs_child = new XMLNode (X_("CapturingSources")); XMLNode* cs_grandchild; - for (vector::iterator i = _capturing_sources.begin(); i != _capturing_sources.end(); ++i) { - cs_grandchild = new XMLNode (X_("file")); - cs_grandchild->add_property (X_("path"), (*i)->path()); - cs_child->add_child_nocopy (*cs_grandchild); - } + cs_grandchild = new XMLNode (X_("file")); + cs_grandchild->add_property (X_("path"), _write_source->path()); + cs_child->add_child_nocopy (*cs_grandchild); /* store the location where capture will start */ Location* pi; - if (_session.get_punch_in() && ((pi = _session.locations()->auto_punch_location()) != 0)) { + if (Config->get_punch_in() && ((pi = _session.locations()->auto_punch_location()) != 0)) { snprintf (buf, sizeof (buf), "%" PRIu32, pi->start()); } else { snprintf (buf, sizeof (buf), "%" PRIu32, _session.transport_frame()); @@ -480,9 +1252,10 @@ MidiDiskstream::set_state (const XMLNode& node) in_set_state = true; for (niter = nlist.begin(); niter != nlist.end(); ++niter) { - if ((*niter)->name() == IO::state_node_name) { + /*if ((*niter)->name() == IO::state_node_name) { deprecated_io_node = new XMLNode (**niter); - } + }*/ + assert ((*niter)->name() != IO::state_node_name); if ((*niter)->name() == X_("CapturingSources")) { capture_pending_node = *niter; @@ -497,20 +1270,29 @@ MidiDiskstream::set_state (const XMLNode& node) _name = prop->value(); } - if (deprecated_io_node) { - if ((prop = deprecated_io_node->property ("id")) != 0) { - sscanf (prop->value().c_str(), "%" PRIu64, &_id); - } - } else { - if ((prop = node.property ("id")) != 0) { - sscanf (prop->value().c_str(), "%" PRIu64, &_id); - } + if ((prop = node.property ("id")) != 0) { + _id = prop->value (); } if ((prop = node.property ("flags")) != 0) { - _flags = strtol (prop->value().c_str(), 0, 0); + _flags = Flag (string_2_enum (prop->value(), _flags)); + } + + ChannelMode channel_mode = AllChannels; + if ((prop = node.property ("channel-mode")) != 0) { + channel_mode = ChannelMode (string_2_enum(prop->value(), channel_mode)); + } + + unsigned int channel_mask = 0xFFFF; + if ((prop = node.property ("channel-mask")) != 0) { + sscanf (prop->value().c_str(), "0x%x", &channel_mask); + if (channel_mask & (~0xFFFF)) { + warning << _("MidiDiskstream: XML property channel-mask out of range") << endmsg; + } } + set_channel_mode(channel_mode, channel_mask); + if ((prop = node.property ("channels")) != 0) { nchans = atoi (prop->value().c_str()); } @@ -530,11 +1312,7 @@ MidiDiskstream::set_state (const XMLNode& node) _playlist->set_orig_diskstream_id (_id); } - if (!destructive() && capture_pending_node) { - /* destructive streams have one and only one source per channel, - and so they never end up in pending capture in any useful - sense. - */ + if (capture_pending_node) { use_pending_capture_data (*capture_pending_node); } @@ -552,7 +1330,8 @@ MidiDiskstream::set_state (const XMLNode& node) /* make sure this is clear before we do anything else */ - _capturing_sources.clear (); + // FIXME? + //_capturing_source = 0; /* write sources are handled when we handle the input set up of the IO that owns this DS (::non_realtime_input_change()) @@ -566,22 +1345,70 @@ MidiDiskstream::set_state (const XMLNode& node) int MidiDiskstream::use_new_write_source (uint32_t n) { + if (!recordable()) { + return 1; + } + + assert(n == 0); + + if (_write_source) { + + if (_write_source->is_empty ()) { + _write_source->mark_for_remove (); + _write_source.reset(); + } else { + _write_source.reset(); + } + } + + try { + _write_source = boost::dynamic_pointer_cast(_session.create_midi_source_for_session (*this)); + if (!_write_source) { + throw failed_constructor(); + } + } + + catch (failed_constructor &err) { + error << string_compose (_("%1:%2 new capture file not initialized correctly"), _name, n) << endmsg; + _write_source.reset(); + return -1; + } + + _write_source->set_allow_remove_if_empty (true); + return 0; } void MidiDiskstream::reset_write_sources (bool mark_write_complete, bool force) { + if (!recordable()) { + return; + } + + if (_write_source && mark_write_complete) { + _write_source->mark_streaming_write_completed (); + } + + use_new_write_source (0); + + if (record_enabled()) { + //_capturing_sources.push_back (_write_source); + } } int MidiDiskstream::rename_write_sources () { + if (_write_source != 0) { + _write_source->set_source_name (_name, destructive()); + /* XXX what to do if this fails ? */ + } return 0; } void -MidiDiskstream::set_block_size (jack_nframes_t nframes) +MidiDiskstream::set_block_size (nframes_t nframes) { } @@ -593,29 +1420,91 @@ MidiDiskstream::allocate_temporary_buffers () void MidiDiskstream::monitor_input (bool yn) { + if (_source_port) + _source_port->request_monitor_input (yn); + else + cerr << "MidiDiskstream NO SOURCE PORT TO MONITOR\n"; } void MidiDiskstream::set_align_style_from_io () { + bool have_physical = false; + + if (_io == 0) { + return; + } + + get_input_sources (); + + if (_source_port && _source_port->flags() & JackPortIsPhysical) { + have_physical = true; + } + + if (have_physical) { + set_align_style (ExistingMaterial); + } else { + set_align_style (CaptureTime); + } } float MidiDiskstream::playback_buffer_load () const { - return 0; + return (float) ((double) _playback_buf->read_space()/ + (double) _playback_buf->capacity()); } float MidiDiskstream::capture_buffer_load () const { - return 0; + return (float) ((double) _capture_buf->write_space()/ + (double) _capture_buf->capacity()); } - int MidiDiskstream::use_pending_capture_data (XMLNode& node) { return 0; } + +/** Writes playback events in the given range to \a dst, translating time stamps + * so that an event at \a start has time = 0 + */ +void +MidiDiskstream::get_playback(MidiBuffer& dst, nframes_t start, nframes_t end, nframes_t offset) +{ + if (offset == 0) { + dst.clear(); + assert(dst.size() == 0); + } + + // Reverse. ... We just don't do reverse, ok? Back off. + if (end <= start) { + return; + } + + // Check only events added this offset cycle + MidiBuffer::iterator this_cycle_start = dst.end(); + + // Translates stamps to be relative to start, but add offset. + #if 1 + _playback_buf->read(dst, start, end, offset); + #else + const size_t events_read = _playback_buf->read(dst, start, end, offset); + cout << "frames read = " << frames_read << " events read = " << events_read + << " end = " << end << " start = " << start << " offset = " << offset + << " readspace " << _playback_buf->read_space() + << " writespace " << _playback_buf->write_space() << endl; + #endif + + gint32 frames_read = end - start; + g_atomic_int_add(&_frames_read_from_ringbuffer, frames_read); + + // Feed the data through the MidiStateTracker + // If it detects a LoopEvent it will add necessary note offs + if (_midi_state_tracker.track(this_cycle_start, dst.end())) { + _midi_state_tracker.resolve_notes(dst, end-start - 1 + offset); + } +}