/* This file is part of Machina. Copyright 2007-2013 David Robillard Machina is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Machina is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Machina. If not, see . */ #include #include #include #include #include #include #include #include #include "SMFWriter.hpp" using std::endl; namespace machina { /** Create a new SMF writer. * * @param unit Must match the time stamp of ALL events passed to write, or * terrible things will happen. * * *** NOTE: Only beat time is implemented currently. */ SMFWriter::SMFWriter(Raul::TimeUnit unit) : _fd(NULL) , _unit(unit) , _start_time(unit, 0, 0) , _last_ev_time(unit, 0, 0) , _track_size(0) , _header_size(0) { if (unit.type() == Raul::TimeUnit::BEATS) { assert(unit.ppt() < std::numeric_limits::max()); } } SMFWriter::~SMFWriter() { if (_fd) { finish(); } } /** Start a write to an SMF file. * * @param filename Filename to write to. * @param start_time Beat time corresponding to t=0 in the file (timestamps * passed to write_event will have this value subtracted before writing). */ bool SMFWriter::start(const std::string& filename, Raul::TimeStamp start_time) throw (std::logic_error) { if (_fd) { throw std::logic_error( "Attempt to start new write while write in progress."); } std::cout << "Opening SMF file " << filename << " for writing." << endl; _fd = fopen(filename.c_str(), "w+"); if (_fd) { _track_size = 0; _filename = filename; _start_time = start_time; _last_ev_time = 0; // write a tentative header to pad file out so writing starts at the right offset write_header(); } return (_fd == 0) ? -1 : 0; } /** Write an event at the end of the file. * * @param time The absolute time of the event, relative to the start of the * file (the start_time parameter to start). Must be monotonically increasing * on successive calls to this method. */ void SMFWriter::write_event(Raul::TimeStamp time, size_t ev_size, const unsigned char* ev) throw (std::logic_error) { if (time < _start_time) { throw std::logic_error("Event time is before file start time"); } else if (time < _last_ev_time) { throw std::logic_error("Event time not monotonically increasing"); } else if (time.unit() != _unit) { throw std::logic_error("Event has unexpected time unit"); } Raul::TimeStamp delta_time = time; delta_time -= _start_time; fseek(_fd, 0, SEEK_END); uint64_t delta_ticks = delta_time.ticks() * _unit.ppt() + delta_time.subticks(); size_t stamp_size = 0; /* If delta time is too long (i.e. overflows), write out empty * "proprietary" events to reach the desired time. * Any SMF reading application should interpret this correctly * (by accumulating the delta time and ignoring the event) */ while (delta_ticks > VAR_LEN_MAX) { static unsigned char null_event[] = { 0xFF, 0x7F, 0x0 }; stamp_size = write_var_len(VAR_LEN_MAX); fwrite(null_event, 1, 3, _fd); _track_size += stamp_size + 3; delta_ticks -= VAR_LEN_MAX; } assert(delta_ticks <= VAR_LEN_MAX); stamp_size = write_var_len(static_cast(delta_ticks)); fwrite(ev, 1, ev_size, _fd); _last_ev_time = time; _track_size += stamp_size + ev_size; } void SMFWriter::flush() { if (_fd) { fflush(_fd); } } void SMFWriter::finish() throw (std::logic_error) { if (!_fd) { throw std::logic_error( "Attempt to finish write with no write in progress."); } write_footer(); fclose(_fd); _fd = NULL; } void SMFWriter::write_header() { std::cout << "SMF Flushing header\n"; const uint16_t type = htons(0); // SMF Type 0 (single track) const uint16_t ntracks = htons(1); // Number of tracks (always 1 for Type 0) const uint16_t division = htons(_unit.ppt()); // PPQN char data[6]; memcpy(data, &type, 2); memcpy(data + 2, &ntracks, 2); memcpy(data + 4, &division, 2); //data[4] = _ppqn & 0xF0; //data[5] = _ppqn & 0x0F; _fd = freopen(_filename.c_str(), "r+", _fd); assert(_fd); fseek(_fd, 0, 0); write_chunk("MThd", 6, data); write_chunk_header("MTrk", _track_size); } void SMFWriter::write_footer() { std::cout << "Writing EOT\n"; fseek(_fd, 0, SEEK_END); write_var_len(1); // whatever... static const unsigned char eot[4] = { 0xFF, 0x2F, 0x00 }; // end-of-track meta-event fwrite(eot, 1, 4, _fd); } void SMFWriter::write_chunk_header(const char id[4], uint32_t length) { const uint32_t length_be = htonl(length); fwrite(id, 1, 4, _fd); fwrite(&length_be, 4, 1, _fd); } void SMFWriter::write_chunk(const char id[4], uint32_t length, void* data) { write_chunk_header(id, length); fwrite(data, 1, length, _fd); } /** Write an SMF variable length value. * * @return size (in bytes) of the value written. */ size_t SMFWriter::write_var_len(uint32_t value) { size_t ret = 0; uint32_t buffer = value & 0x7F; while ((value >>= 7)) { buffer <<= 8; buffer |= ((value & 0x7F) | 0x80); } while (true) { //printf("Writing var len byte %X\n", (unsigned char)buffer); ++ret; fputc(buffer, _fd); if (buffer & 0x80) { buffer >>= 8; } else { break; } } return ret; } } // namespace machina