Mercurial > hg > svcore
diff data/fileio/MIDIFileWriter.cpp @ 301:73537d900d4b
* Add MIDI file export (closes FR#1643721)
author | Chris Cannam |
---|---|
date | Thu, 04 Oct 2007 11:52:38 +0000 |
parents | |
children | 516819f2b97b |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/data/fileio/MIDIFileWriter.cpp Thu Oct 04 11:52:38 2007 +0000 @@ -0,0 +1,431 @@ +/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ + +/* + Sonic Visualiser + An audio file viewer and annotation editor. + Centre for Digital Music, Queen Mary, University of London. + + This program 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 2 of the + License, or (at your option) any later version. See the file + COPYING included with this distribution for more information. +*/ + + +/* + This is a modified version of a source file from the + Rosegarden MIDI and audio sequencer and notation editor. + This file copyright 2000-2007 Richard Bown and Chris Cannam + and copyright 2007 QMUL. +*/ + +#include "MIDIFileWriter.h" + +#include "MIDIEvent.h" + +#include "model/NoteModel.h" + +#include "base/Pitch.h" + +#include <algorithm> +#include <fstream> + +using std::ofstream; +using std::string; +using std::ios; + +using namespace MIDIConstants; + +MIDIFileWriter::MIDIFileWriter(QString path, NoteModel *model, float tempo) : + m_path(path), + m_model(model), + m_modelUsesHz(false), + m_tempo(tempo) +{ + if (model->getScaleUnits().toLower() == "hz") m_modelUsesHz = true; + + if (!convert()) { + m_error = "Conversion from model to internal MIDI format failed"; + } +} + +MIDIFileWriter::~MIDIFileWriter() +{ + for (MIDIComposition::iterator i = m_midiComposition.begin(); + i != m_midiComposition.end(); ++i) { + + for (MIDITrack::iterator j = i->second.begin(); + j != i->second.end(); ++j) { + delete *j; + } + + i->second.clear(); + } + + m_midiComposition.clear(); +} + +bool +MIDIFileWriter::isOK() const +{ + return m_error == ""; +} + +QString +MIDIFileWriter::getError() const +{ + return m_error; +} + +void +MIDIFileWriter::write() +{ + writeComposition(); +} + +string +MIDIFileWriter::intToMIDIBytes(int number) const +{ + MIDIByte upper; + MIDIByte lower; + + upper = (number & 0xFF00) >> 8; + lower = (number & 0x00FF); + + string rv; + rv += upper; + rv += lower; + return rv; +} + +string +MIDIFileWriter::longToMIDIBytes(unsigned long number) const +{ + MIDIByte upper1; + MIDIByte lower1; + MIDIByte upper2; + MIDIByte lower2; + + upper1 = (number & 0xff000000) >> 24; + lower1 = (number & 0x00ff0000) >> 16; + upper2 = (number & 0x0000ff00) >> 8; + lower2 = (number & 0x000000ff); + + string rv; + rv += upper1; + rv += lower1; + rv += upper2; + rv += lower2; + return rv; +} + +// Turn a delta time into a MIDI time - overlapping into +// a maximum of four bytes using the MSB as the carry on +// flag. +// +string +MIDIFileWriter::longToVarBuffer(unsigned long number) const +{ + string rv; + + long inNumber = number; + long outNumber; + + // get the lowest 7 bits of the number + outNumber = number & 0x7f; + + // Shift and test and move the numbers + // on if we need them - setting the MSB + // as we go. + // + while ((inNumber >>= 7 ) > 0) { + outNumber <<= 8; + outNumber |= 0x80; + outNumber += (inNumber & 0x7f); + } + + // Now move the converted number out onto the buffer + // + while (true) { + rv += (MIDIByte)(outNumber & 0xff); + if (outNumber & 0x80) + outNumber >>= 8; + else + break; + } + + return rv; +} + +bool +MIDIFileWriter::writeHeader() +{ + *m_midiFile << MIDI_FILE_HEADER; + + // Number of bytes in header + *m_midiFile << (MIDIByte) 0x00; + *m_midiFile << (MIDIByte) 0x00; + *m_midiFile << (MIDIByte) 0x00; + *m_midiFile << (MIDIByte) 0x06; + + // File format + *m_midiFile << (MIDIByte) 0x00; + *m_midiFile << (MIDIByte) m_format; + + *m_midiFile << intToMIDIBytes(m_numberOfTracks); + + *m_midiFile << intToMIDIBytes(m_timingDivision); + + return true; +} + +bool +MIDIFileWriter::writeTrack(int trackNumber) +{ + bool retOK = true; + MIDIByte eventCode = 0; + MIDITrack::iterator midiEvent; + + // First we write into the trackBuffer, then write it out to the + // file with its accompanying length. + // + string trackBuffer; + + for (midiEvent = m_midiComposition[trackNumber].begin(); + midiEvent != m_midiComposition[trackNumber].end(); + midiEvent++) { + + // Write the time to the buffer in MIDI format + trackBuffer += longToVarBuffer((*midiEvent)->getTime()); + + if ((*midiEvent)->isMeta()) { + trackBuffer += MIDI_FILE_META_EVENT; + trackBuffer += (*midiEvent)->getMetaEventCode(); + + // Variable length number field + trackBuffer += longToVarBuffer((*midiEvent)-> + getMetaMessage().length()); + + trackBuffer += (*midiEvent)->getMetaMessage(); + } else { + // Send the normal event code (with encoded channel information) + if (((*midiEvent)->getEventCode() != eventCode) || + ((*midiEvent)->getEventCode() == MIDI_SYSTEM_EXCLUSIVE)) { + trackBuffer += (*midiEvent)->getEventCode(); + eventCode = (*midiEvent)->getEventCode(); + } + + // Send the relevant data + // + switch ((*midiEvent)->getMessageType()) { + case MIDI_NOTE_ON: + case MIDI_NOTE_OFF: + case MIDI_POLY_AFTERTOUCH: + trackBuffer += (*midiEvent)->getData1(); + trackBuffer += (*midiEvent)->getData2(); + break; + + case MIDI_CTRL_CHANGE: + trackBuffer += (*midiEvent)->getData1(); + trackBuffer += (*midiEvent)->getData2(); + break; + + case MIDI_PROG_CHANGE: + trackBuffer += (*midiEvent)->getData1(); + break; + + case MIDI_CHNL_AFTERTOUCH: + trackBuffer += (*midiEvent)->getData1(); + break; + + case MIDI_PITCH_BEND: + trackBuffer += (*midiEvent)->getData1(); + trackBuffer += (*midiEvent)->getData2(); + break; + + case MIDI_SYSTEM_EXCLUSIVE: + // write out message length + trackBuffer += + longToVarBuffer((*midiEvent)->getMetaMessage().length()); + + // now the message + trackBuffer += (*midiEvent)->getMetaMessage(); + break; + + default: + break; + } + } + } + + // Now we write the track - First the standard header.. + // + *m_midiFile << MIDI_TRACK_HEADER; + + // ..now the length of the buffer.. + // + *m_midiFile << longToMIDIBytes((long)trackBuffer.length()); + + // ..then the buffer itself.. + // + *m_midiFile << trackBuffer; + + return retOK; +} + +bool +MIDIFileWriter::writeComposition() +{ + bool retOK = true; + + m_midiFile = + new ofstream(m_path.toLocal8Bit().data(), ios::out | ios::binary); + + if (!(*m_midiFile)) { + m_error = "Can't open file for writing."; + delete m_midiFile; + m_midiFile = 0; + return false; + } + + if (!writeHeader()) { + retOK = false; + } + + for (unsigned int i = 0; i < m_numberOfTracks; i++) { + if (!writeTrack(i)) { + retOK = false; + } + } + + m_midiFile->close(); + delete m_midiFile; + m_midiFile = 0; + + if (!retOK) { + m_error = "MIDI file write failed"; + } + + return retOK; +} + +bool +MIDIFileWriter::convert() +{ + m_timingDivision = 480; + m_format = MIDI_SINGLE_TRACK_FILE; + m_numberOfTracks = 1; + + int track = 0; + int midiChannel = 0; + + MIDIEvent *event; + + event = new MIDIEvent(0, MIDI_FILE_META_EVENT, MIDI_CUE_POINT, + "Exported from Sonic Visualiser"); + m_midiComposition[track].push_back(event); + + event = new MIDIEvent(0, MIDI_FILE_META_EVENT, MIDI_CUE_POINT, + "http://www.sonicvisualiser.org/"); + m_midiComposition[track].push_back(event); + + long tempoValue = long(60000000.0 / m_tempo + 0.01); + string tempoString; + tempoString += (MIDIByte)(tempoValue >> 16 & 0xFF); + tempoString += (MIDIByte)(tempoValue >> 8 & 0xFF); + tempoString += (MIDIByte)(tempoValue & 0xFF); + + event = new MIDIEvent(0, MIDI_FILE_META_EVENT, MIDI_SET_TEMPO, + tempoString); + m_midiComposition[track].push_back(event); + + // Omit time signature + + const NoteModel::PointList ¬es = + static_cast<SparseModel<Note> *>(m_model)->getPoints(); + + for (NoteModel::PointList::const_iterator i = notes.begin(); + i != notes.end(); ++i) { + + long frame = i->frame; + float value = i->value; + size_t duration = i->duration; + + int pitch; + + if (m_modelUsesHz) { + pitch = Pitch::getPitchForFrequency(value); + } else { + pitch = lrintf(value); + } + + if (pitch < 0) pitch = 0; + if (pitch > 127) pitch = 127; + + // Convert frame to MIDI time + + double seconds = double(frame) / double(m_model->getSampleRate()); + double quarters = (seconds * m_tempo) / 60.0; + unsigned long midiTime = lrint(quarters * m_timingDivision); + + // We don't support velocity in note models yet + int velocity = 100; + + // Get the sounding time for the matching NOTE_OFF + seconds = double(frame + duration) / double(m_model->getSampleRate()); + quarters = (seconds * m_tempo) / 60.0; + unsigned long endTime = lrint(quarters * m_timingDivision); + + // At this point all the notes we insert have absolute times + // in the delta time fields. We resolve these into delta + // times further down (can't do it until all the note offs are + // in place). + + event = new MIDIEvent(midiTime, + MIDI_NOTE_ON | midiChannel, + pitch, + velocity); + m_midiComposition[track].push_back(event); + + event = new MIDIEvent(endTime, + MIDI_NOTE_OFF | midiChannel, + pitch, + 127); // loudest silence you can muster + + m_midiComposition[track].push_back(event); + } + + // Now gnash through the MIDI events and turn the absolute times + // into delta times. + // + for (unsigned int i = 0; i < m_numberOfTracks; i++) { + + unsigned long lastMidiTime = 0; + + // First sort the track with the MIDIEvent comparator. Use + // stable_sort so that events with equal times are maintained + // in their current order. + // + std::stable_sort(m_midiComposition[i].begin(), + m_midiComposition[i].end(), + MIDIEventCmp()); + + for (MIDITrack::iterator it = m_midiComposition[i].begin(); + it != m_midiComposition[i].end(); it++) { + unsigned long deltaTime = (*it)->getTime() - lastMidiTime; + lastMidiTime = (*it)->getTime(); + (*it)->setTime(deltaTime); + } + + // Insert end of track event (delta time = 0) + // + event = new MIDIEvent(0, MIDI_FILE_META_EVENT, + MIDI_END_OF_TRACK, ""); + + m_midiComposition[i].push_back(event); + } + + return true; +} +