mirror of
https://github.com/cookiengineer/audacity
synced 2026-02-07 20:22:13 +01:00
Extensive changes to improve NoteTrack display and (some) editing, NoteTrack playback via MIDI, and Midi-to-Audio alignment.
This commit is contained in:
@@ -21,16 +21,79 @@
|
||||
|
||||
#include "Audacity.h"
|
||||
|
||||
#if defined(USE_MIDI)
|
||||
#include <sstream>
|
||||
|
||||
#include "allegro.h"
|
||||
/* REQUIRES PORTMIDI */
|
||||
//#include "portmidi.h"
|
||||
//#include "porttime.h"
|
||||
#if defined(USE_MIDI)
|
||||
#define ROUND(x) ((int) ((x) + 0.5))
|
||||
|
||||
#include "AColor.h"
|
||||
#include "NoteTrack.h"
|
||||
#include "DirManager.h"
|
||||
#include "Internat.h"
|
||||
#include "Prefs.h"
|
||||
|
||||
#ifdef SONIFY
|
||||
#include "portmidi.h"
|
||||
|
||||
#define SON_PROGRAM 0
|
||||
#define SON_AutoSave 67
|
||||
#define SON_ModifyState 60
|
||||
#define SON_NoteBackground 72
|
||||
#define SON_NoteForeground 74
|
||||
#define SON_Measures 76 /* "bar line" */
|
||||
#define SON_Serialize 77
|
||||
#define SON_Unserialize 79
|
||||
#define SON_VEL 100
|
||||
|
||||
|
||||
PmStream *sonMidiStream;
|
||||
bool sonificationStarted = false;
|
||||
|
||||
void SonifyBeginSonification()
|
||||
{
|
||||
PmError err = Pm_OpenOutput(&sonMidiStream, Pm_GetDefaultOutputDeviceID(),
|
||||
NULL, 0, NULL, NULL, 0);
|
||||
if (err) sonMidiStream = NULL;
|
||||
if (sonMidiStream)
|
||||
Pm_WriteShort(sonMidiStream, 0, Pm_Message(0xC0, SON_PROGRAM, 0));
|
||||
sonificationStarted = true;
|
||||
}
|
||||
|
||||
|
||||
void SonifyEndSonification()
|
||||
{
|
||||
if (sonMidiStream) Pm_Close(sonMidiStream);
|
||||
sonificationStarted = false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
void SonifyNoteOnOff(int p, int v)
|
||||
{
|
||||
if (!sonificationStarted)
|
||||
SonifyBeginSonification();
|
||||
if (sonMidiStream)
|
||||
Pm_WriteShort(sonMidiStream, 0, Pm_Message(0x90, p, v));
|
||||
}
|
||||
|
||||
#define SONFNS(name) \
|
||||
void SonifyBegin ## name() { SonifyNoteOnOff(SON_ ## name, SON_VEL); } \
|
||||
void SonifyEnd ## name() { SonifyNoteOnOff(SON_ ## name, 0); }
|
||||
|
||||
SONFNS(NoteBackground)
|
||||
SONFNS(NoteForeground)
|
||||
SONFNS(Measures)
|
||||
SONFNS(Serialize)
|
||||
SONFNS(Unserialize)
|
||||
SONFNS(ModifyState)
|
||||
SONFNS(AutoSave)
|
||||
|
||||
#undef SONFNS
|
||||
|
||||
#endif
|
||||
|
||||
|
||||
|
||||
NoteTrack *TrackFactory::NewNoteTrack()
|
||||
{
|
||||
@@ -44,13 +107,15 @@ Track(projDirManager)
|
||||
SetName(GetDefaultName());
|
||||
|
||||
mSeq = NULL;
|
||||
mLen = 0.0;
|
||||
mSerializationBuffer = NULL;
|
||||
mSerializationLength = 0;
|
||||
|
||||
mDirManager = projDirManager;
|
||||
|
||||
mGain = 0;
|
||||
|
||||
mBottomNote = 24;
|
||||
mPitchHeight = 5;
|
||||
|
||||
mVisibleChannels = 0xFFFF;
|
||||
mLastMidiPosition = 0;
|
||||
@@ -70,48 +135,69 @@ NoteTrack::~NoteTrack()
|
||||
Track *NoteTrack::Duplicate()
|
||||
{
|
||||
NoteTrack *duplicate = new NoteTrack(mDirManager);
|
||||
duplicate->Init(*this);
|
||||
// Duplicate on NoteTrack moves data from mSeq to mSerializationBuffer
|
||||
// and from mSerializationBuffer to mSeq on alternate calls. Duplicate
|
||||
// to the undo stack and Duplicate back to the project should result
|
||||
// in serialized blobs on the undo stack and traversable data in the
|
||||
// project object.
|
||||
if (mSeq) {
|
||||
SonifyBeginSerialize();
|
||||
assert(!mSerializationBuffer);
|
||||
// serialize from this to duplicate's mSerializationBuffer
|
||||
mSeq->serialize((void**)&duplicate->mSerializationBuffer,
|
||||
&duplicate->mSerializationLength);
|
||||
SonifyEndSerialize();
|
||||
} else if (mSerializationBuffer) {
|
||||
SonifyBeginUnserialize();
|
||||
assert(!mSeq);
|
||||
Alg_track_ptr alg_track = Alg_seq::unserialize(mSerializationBuffer,
|
||||
mSerializationLength);
|
||||
assert(alg_track->get_type() == 's');
|
||||
duplicate->mSeq = (Alg_seq_ptr) alg_track;
|
||||
SonifyEndUnserialize();
|
||||
} else assert(false); // bug if neither mSeq nor mSerializationBuffer
|
||||
// copy some other fields here
|
||||
duplicate->mBottomNote = mBottomNote;
|
||||
duplicate->SetBottomNote(mBottomNote);
|
||||
duplicate->SetPitchHeight(mPitchHeight);
|
||||
duplicate->mLastMidiPosition = mLastMidiPosition;
|
||||
duplicate->mLen = mLen;
|
||||
duplicate->mVisibleChannels = mVisibleChannels;
|
||||
duplicate->SetOffset(GetOffset());
|
||||
duplicate->SetGain(GetGain());
|
||||
|
||||
return duplicate;
|
||||
}
|
||||
|
||||
void NoteTrack::DrawLabelControls(wxDC & dc, wxRect & r)
|
||||
|
||||
double NoteTrack::GetStartTime()
|
||||
{
|
||||
return GetOffset();
|
||||
}
|
||||
|
||||
|
||||
double NoteTrack::GetEndTime()
|
||||
{
|
||||
return GetStartTime() + (mSeq ? mSeq->get_real_dur() : 0.0);
|
||||
}
|
||||
|
||||
|
||||
int NoteTrack::DrawLabelControls(wxDC & dc, wxRect & r)
|
||||
{
|
||||
int wid = 23;
|
||||
int ht = 16;
|
||||
|
||||
if (r.height < ht * 4) {
|
||||
return;
|
||||
return r.y + 5 + ht * 4;
|
||||
}
|
||||
|
||||
int x = r.x + (r.width / 2 - wid * 2) + 2;
|
||||
int y = r.y + 5;
|
||||
|
||||
wxRect box;
|
||||
for (int row = 0; row < 4; row++) {
|
||||
for (int col = 0; col < 4; col++) {
|
||||
int channel = row * 4 + col + 1;
|
||||
|
||||
wxRect box;
|
||||
box.x = x + col * wid;
|
||||
box.y = y + row * ht;
|
||||
box.width = wid;
|
||||
@@ -147,6 +233,7 @@ void NoteTrack::DrawLabelControls(wxDC & dc, wxRect & r)
|
||||
dc.DrawText(t, box.x + (box.width - w) / 2, box.y + (box.height - h) / 2);
|
||||
}
|
||||
}
|
||||
return box.GetBottom();
|
||||
}
|
||||
|
||||
bool NoteTrack::LabelClick(wxRect & r, int mx, int my, bool right)
|
||||
@@ -159,6 +246,8 @@ bool NoteTrack::LabelClick(wxRect & r, int mx, int my, bool right)
|
||||
|
||||
int x = r.x + (r.width / 2 - wid * 2);
|
||||
int y = r.y + 1;
|
||||
// after adding Mute and Solo buttons, mapping is broken, so hack in the offset
|
||||
y += 12;
|
||||
|
||||
int col = (mx - x) / wid;
|
||||
int row = (my - y) / ht;
|
||||
@@ -185,7 +274,6 @@ void NoteTrack::SetSequence(Alg_seq_ptr seq)
|
||||
delete mSeq;
|
||||
|
||||
mSeq = seq;
|
||||
mLen = (seq ? seq->get_real_dur() : 0.0);
|
||||
}
|
||||
|
||||
Alg_seq* NoteTrack::GetSequence()
|
||||
@@ -268,10 +356,8 @@ bool NoteTrack::Cut(double t0, double t1, Track **dest){
|
||||
newTrack->Init(*this);
|
||||
|
||||
mSeq->convert_to_seconds();
|
||||
newTrack->mSeq = mSeq->cut(t0, len, false);
|
||||
|
||||
mLen = (mLen <= len ? 0.0 : mLen - len);
|
||||
newTrack->mLen = len;
|
||||
newTrack->mSeq = mSeq->cut(t0 - GetOffset(), len, false);
|
||||
newTrack->SetOffset(GetOffset());
|
||||
|
||||
// What should be done with the rest of newTrack's members?
|
||||
//(mBottomNote, mDirManager, mLastMidiPosition,
|
||||
@@ -281,6 +367,7 @@ bool NoteTrack::Cut(double t0, double t1, Track **dest){
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool NoteTrack::Copy(double t0, double t1, Track **dest){
|
||||
|
||||
//dest goes onto clipboard
|
||||
@@ -294,8 +381,8 @@ bool NoteTrack::Copy(double t0, double t1, Track **dest){
|
||||
newTrack->Init(*this);
|
||||
|
||||
mSeq->convert_to_seconds();
|
||||
newTrack->mSeq = mSeq->copy(t0, len, false);
|
||||
newTrack->mLen = len;
|
||||
newTrack->mSeq = mSeq->copy(t0 - GetOffset(), len, false);
|
||||
newTrack->SetOffset(GetOffset());
|
||||
|
||||
// What should be done with the rest of newTrack's members?
|
||||
//(mBottomNote, mDirManager, mLastMidiPosition,
|
||||
@@ -305,18 +392,43 @@ bool NoteTrack::Copy(double t0, double t1, Track **dest){
|
||||
|
||||
return true;
|
||||
}
|
||||
bool NoteTrack::Clear(double t0, double t1){
|
||||
|
||||
bool NoteTrack::Trim(double t0, double t1)
|
||||
{
|
||||
if (t1 <= t0)
|
||||
return false;
|
||||
double len = t1 - t0;
|
||||
mSeq->convert_to_seconds();
|
||||
// delete way beyond duration just in case something is out there:
|
||||
mSeq->clear(t1 - GetOffset(), mSeq->get_dur() + 10000.0, false);
|
||||
// Now that stuff beyond selection is cleared, clear before selection:
|
||||
mSeq->clear(0.0, t0 - GetOffset(), false);
|
||||
// want starting time to be t0
|
||||
SetOffset(t0);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool NoteTrack::Clear(double t0, double t1)
|
||||
{
|
||||
// If t1 = t0, should Clear return true?
|
||||
if (t1 <= t0)
|
||||
return false;
|
||||
double len = t1-t0;
|
||||
|
||||
mSeq->clear(t0, len, false);
|
||||
mSeq->clear(t0 - GetOffset(), len, false);
|
||||
|
||||
return true;
|
||||
}
|
||||
bool NoteTrack::Paste(double t, Track *src){
|
||||
|
||||
bool NoteTrack::Paste(double t, Track *src)
|
||||
{
|
||||
// Paste inserts src at time t. If src has a positive offset,
|
||||
// the offset is treated as silence which is also inserted. If
|
||||
// the offset is negative, the offset is ignored and the ENTIRE
|
||||
// src is inserted (otherwise, we would either lose data from
|
||||
// src by not inserting things at negative times, or inserting
|
||||
// things at negative times could overlap things already in
|
||||
// the destination track).
|
||||
|
||||
//Check that src is a non-NULL NoteTrack
|
||||
if (src == NULL || src->GetKind() != Track::Note)
|
||||
@@ -329,27 +441,256 @@ bool NoteTrack::Paste(double t, Track *src){
|
||||
if(!mSeq)
|
||||
mSeq = new Alg_seq();
|
||||
|
||||
mSeq->paste(t, other->mSeq);
|
||||
mLen = mSeq->get_real_dur();
|
||||
if (other->GetOffset() > 0) {
|
||||
mSeq->convert_to_seconds();
|
||||
mSeq->insert_silence(t - GetOffset(), other->GetOffset());
|
||||
t += other->GetOffset();
|
||||
}
|
||||
mSeq->paste(t - GetOffset(), other->mSeq);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Call this function to manipulate the underlying sequence data. This is
|
||||
// NOT the function that handles horizontal dragging.
|
||||
bool NoteTrack::Shift(double t) // t is always seconds
|
||||
{
|
||||
if (t > 0) {
|
||||
// insert an even number of measures
|
||||
mSeq->convert_to_beats();
|
||||
// get initial tempo
|
||||
double tempo = mSeq->get_tempo(0.0);
|
||||
double beats_per_measure = mSeq->get_bar_len(0.0);
|
||||
int m = ROUND(t * tempo / beats_per_measure);
|
||||
// need at least 1 measure, so if we rounded down to zero, fix it
|
||||
if (m == 0) m = 1;
|
||||
// compute new tempo so that m measures at new tempo take t seconds
|
||||
tempo = beats_per_measure * m / t; // in beats per second
|
||||
mSeq->insert_silence(0.0, beats_per_measure * m);
|
||||
mSeq->set_tempo(tempo * 60.0 /* bpm */, 0.0, beats_per_measure * m);
|
||||
mSeq->write("afterShift.gro");
|
||||
} else if (t < 0) {
|
||||
mSeq->convert_to_seconds();
|
||||
mSeq->clear(0, t, true);
|
||||
} else { // offset is zero, no modifications
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
double NoteTrack::NearestBeatTime(double time, double *beat)
|
||||
{
|
||||
wxASSERT(mSeq);
|
||||
// Alg_seq knows nothing about offset, so remove offset time
|
||||
double seq_time = time - GetOffset();
|
||||
seq_time = mSeq->nearest_beat_time(seq_time, beat);
|
||||
// add the offset back in to get "actual" audacity track time
|
||||
return seq_time + GetOffset();
|
||||
}
|
||||
|
||||
bool NoteTrack::StretchRegion(double t0, double t1, double dur)
|
||||
{
|
||||
wxASSERT(mSeq);
|
||||
// Alg_seq::stretch_region uses beats, so we translate time
|
||||
// to beats first:
|
||||
t0 -= GetOffset();
|
||||
t1 -= GetOffset();
|
||||
double b0 = mSeq->get_time_map()->time_to_beat(t0);
|
||||
double b1 = mSeq->get_time_map()->time_to_beat(t1);
|
||||
bool result = mSeq->stretch_region(b0, b1, dur);
|
||||
if (result) {
|
||||
mSeq->convert_to_seconds();
|
||||
mSeq->set_dur(mSeq->get_dur() + dur - (t1 - t0));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Alg_seq_ptr NoteTrack::MakeExportableSeq()
|
||||
{
|
||||
double offset = GetOffset();
|
||||
if (offset == 0)
|
||||
return mSeq;
|
||||
// make a copy, deleting events that are shifted before time 0
|
||||
double start = -offset;
|
||||
if (start < 0) start = 0;
|
||||
// notes that begin before "start" are not included even if they
|
||||
// extend past "start" (because "all" parameter is set to false)
|
||||
Alg_seq_ptr seq = mSeq->copy(start, mSeq->get_dur() - start, false);
|
||||
if (offset > 0) {
|
||||
// swap seq and mSeq so that Shift operates on the new copy
|
||||
Alg_seq_ptr old_seq = mSeq;
|
||||
mSeq = seq;
|
||||
Shift(offset);
|
||||
seq = mSeq; // undo the swap
|
||||
mSeq = old_seq;
|
||||
#ifdef OLD_CODE
|
||||
// now shift events by offset. This must be done with an integer
|
||||
// number of measures, so first, find the beats-per-measure
|
||||
double beats_per_measure = 4.0;
|
||||
Alg_time_sig_ptr tsp = NULL;
|
||||
if (seq->time_sig.length() > 0 && seq->time_sig[0].beat < ALG_EPS) {
|
||||
// there is an initial time signature
|
||||
tsp = &(seq->time_sig[0]);
|
||||
beats_per_measure = (tsp->num * 4) / tsp->den;
|
||||
}
|
||||
// also need the initial tempo
|
||||
double bps = ALG_DEFAULT_BPM / 60;
|
||||
Alg_time_map_ptr map = seq->get_time_map();
|
||||
Alg_beat_ptr bp = &(map->beats[0]);
|
||||
if (bp->time < ALG_EPS) { // tempo change at time 0
|
||||
if (map->beats.len > 1) { // compute slope to get tempo
|
||||
bps = (map->beats[1].beat - map->beats[0].beat) /
|
||||
(map->beats[1].time - map->beats[0].time);
|
||||
} else if (seq->get_time_map()->last_tempo_flag) {
|
||||
bps = seq->get_time_map()->last_tempo;
|
||||
}
|
||||
}
|
||||
// find closest number of measures to fit in the gap
|
||||
// number of measures is offset / measure_time
|
||||
double measure_time = beats_per_measure / bps; // seconds per measure
|
||||
int n = ROUND(offset / measure_time);
|
||||
if (n == 0) n = 1;
|
||||
// we will insert n measures. Compute the desired duration of each.
|
||||
measure_time = offset / n;
|
||||
bps = beats_per_measure / measure_time;
|
||||
// insert integer multiple of measures at beginning
|
||||
seq->convert_to_beats();
|
||||
seq->insert_silence(0, beats_per_measure * n);
|
||||
// make sure time signature at 0 is correct
|
||||
if (tsp) {
|
||||
seq->set_time_sig(0, tsp->num, tsp->den);
|
||||
}
|
||||
// adjust tempo to match offset
|
||||
seq->set_tempo(bps * 60.0, 0, beats_per_measure * n);
|
||||
#endif
|
||||
} else {
|
||||
// if offset is negative, it might not be a multiple of beats, but
|
||||
// we want to preserve the relative positions of measures. I.e. we
|
||||
// should shift barlines and time signatures as well as notes.
|
||||
// Insert a time signature at the first bar-line if necessary.
|
||||
|
||||
// Translate start from seconds to beats and call it beat:
|
||||
double beat = mSeq->get_time_map()->time_to_beat(start);
|
||||
// Find the time signature in mSeq in effect at start (beat):
|
||||
int i = mSeq->time_sig.find_beat(beat);
|
||||
// i is where you would insert a new time sig at beat,
|
||||
// Case 1: beat coincides with a time sig at i. Time signature
|
||||
// at beat means that there is a barline at beat, so when beat
|
||||
// is shifted to 0, the relative barline positions are preserved
|
||||
if (mSeq->time_sig.length() > 0 &&
|
||||
within(beat, mSeq->time_sig[i].beat, ALG_EPS)) {
|
||||
// beat coincides with time signature change, so offset must
|
||||
// be a multiple of beats
|
||||
/* do nothing */ ;
|
||||
// Case 2: there is no time signature before beat.
|
||||
} else if (i == 0 && (mSeq->time_sig.length() == 0 ||
|
||||
mSeq->time_sig[i].beat > beat)) {
|
||||
// If beat does not fall on an implied barline, we need to
|
||||
// insert a time signature.
|
||||
double measures = beat / 4.0;
|
||||
double imeasures = ROUND(measures);
|
||||
if (!within(measures, imeasures, ALG_EPS)) {
|
||||
double bar_offset = (int(measures) + 1) * 4.0 - beat;
|
||||
seq->set_time_sig(bar_offset, 4, 4);
|
||||
}
|
||||
// This case should never be true because if i == 0, either there
|
||||
// are no time signatures before beat (Case 2),
|
||||
// or there is one time signature at beat (Case 1)
|
||||
} else if (i == 0) {
|
||||
/* do nothing (might be good to assert(false)) */ ;
|
||||
// Case 3: i-1 must be the effective time sig position
|
||||
} else {
|
||||
i -= 1; // index the time signature in effect at beat
|
||||
Alg_time_sig_ptr tsp = &(mSeq->time_sig[i]);
|
||||
double beats_per_measure = (tsp->num * 4) / tsp->den;
|
||||
double measures = (beat - tsp->beat) / beats_per_measure;
|
||||
int imeasures = ROUND(measures);
|
||||
if (!within(measures, imeasures, ALG_EPS)) {
|
||||
// beat is not on a measure, so we need to insert a time sig
|
||||
// to force a bar line at the first measure location after
|
||||
// beat
|
||||
double bar = tsp->beat + beats_per_measure * (int(measures) + 1);
|
||||
double bar_offset = bar - beat;
|
||||
// insert new time signature at bar_offset in new sequence
|
||||
// It will have the same time signature, but the position will
|
||||
// force a barline to match the barlines in mSeq
|
||||
seq->set_time_sig(bar_offset, tsp->num, tsp->den);
|
||||
}
|
||||
// else beat coincides with a barline, so no need for an extra
|
||||
// time signature to force barline alignment
|
||||
}
|
||||
}
|
||||
return seq;
|
||||
}
|
||||
|
||||
|
||||
bool NoteTrack::ExportMIDI(wxString f)
|
||||
{
|
||||
return mSeq->smf_write(f.mb_str());
|
||||
Alg_seq_ptr seq = MakeExportableSeq();
|
||||
bool rslt = seq->smf_write(f.mb_str());
|
||||
if (seq != mSeq) delete seq;
|
||||
return rslt;
|
||||
}
|
||||
|
||||
bool NoteTrack::ExportAllegro(wxString f)
|
||||
{
|
||||
return mSeq->write(f.mb_str());
|
||||
double offset = GetOffset();
|
||||
bool in_seconds;
|
||||
gPrefs->Read(wxT("/FileFormats/AllegroStyle"), &in_seconds, true);
|
||||
if (in_seconds) {
|
||||
mSeq->convert_to_seconds();
|
||||
} else {
|
||||
mSeq->convert_to_beats();
|
||||
}
|
||||
return mSeq->write(f.mb_str(), offset);
|
||||
}
|
||||
|
||||
|
||||
bool NoteTrack::HandleXMLTag(const wxChar *tag, const wxChar **attrs)
|
||||
{
|
||||
if (!wxStrcmp(tag, wxT("notetrack"))) {
|
||||
while (*attrs) {
|
||||
const wxChar *attr = *attrs++;
|
||||
const wxChar *value = *attrs++;
|
||||
if (!value)
|
||||
break;
|
||||
const wxString strValue = value;
|
||||
long nValue;
|
||||
double dblValue;
|
||||
if (!wxStrcmp(attr, wxT("name")) && XMLValueChecker::IsGoodString(strValue))
|
||||
mName = strValue;
|
||||
else if (!wxStrcmp(attr, wxT("offset")) &&
|
||||
XMLValueChecker::IsGoodString(strValue) &&
|
||||
Internat::CompatibleToDouble(strValue, &dblValue))
|
||||
SetOffset(dblValue);
|
||||
else if (!wxStrcmp(attr, wxT("visiblechannels"))) {
|
||||
if (!XMLValueChecker::IsGoodInt(strValue) ||
|
||||
!strValue.ToLong(&nValue) ||
|
||||
!XMLValueChecker::IsValidVisibleChannels(nValue))
|
||||
return false;
|
||||
mVisibleChannels = nValue;
|
||||
}
|
||||
else if (!wxStrcmp(attr, wxT("height")) &&
|
||||
XMLValueChecker::IsGoodInt(strValue) && strValue.ToLong(&nValue))
|
||||
mHeight = nValue;
|
||||
else if (!wxStrcmp(attr, wxT("minimized")) &&
|
||||
XMLValueChecker::IsGoodInt(strValue) && strValue.ToLong(&nValue))
|
||||
mMinimized = (nValue != 0);
|
||||
else if (!wxStrcmp(attr, wxT("velocity")) &&
|
||||
XMLValueChecker::IsGoodString(strValue) &&
|
||||
Internat::CompatibleToDouble(strValue, &dblValue))
|
||||
mGain = (float) dblValue;
|
||||
else if (!wxStrcmp(attr, wxT("bottomnote")) &&
|
||||
XMLValueChecker::IsGoodInt(strValue) && strValue.ToLong(&nValue))
|
||||
SetBottomNote(nValue);
|
||||
else if (!wxStrcmp(attr, wxT("data"))) {
|
||||
std::string s(wxString::FromUTF8(strValue.c_str()));
|
||||
std::istringstream data(s);
|
||||
mSeq = new Alg_seq(data, false);
|
||||
}
|
||||
} // while
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -360,6 +701,103 @@ XMLTagHandler *NoteTrack::HandleXMLChild(const wxChar *tag)
|
||||
|
||||
void NoteTrack::WriteXML(XMLWriter &xmlFile)
|
||||
{
|
||||
std::ostringstream data;
|
||||
// Normally, Duplicate is called in pairs -- once to put NoteTrack
|
||||
// on the Undo stack, and again to move from the Undo stack to an
|
||||
// "active" editable state. For efficiency, we do not do a "real"
|
||||
// Duplicate followed by serialization into a binary blob. Instead,
|
||||
// we combine the Duplicate with serialization or unserialization.
|
||||
// Serialization and Unserialization happen on alternate calls to
|
||||
// Duplicate and (usually) produce the right results at the right
|
||||
// time.
|
||||
// It turns out that this optimized Duplicate is a little too
|
||||
// clever. There is at least one case where a track can be duplicated
|
||||
// and then AutoSave'd. (E.g. do an "Insert Silence" effect on a
|
||||
// NoteTrack.) In this case, mSeq will be NULL. To avoid a crash
|
||||
// and perform WriteXML, we may need to restore NoteTracks from binary
|
||||
// blobs to regular data structures (with an Alg_seq member).
|
||||
NoteTrack *saveme = this;
|
||||
if (!mSeq) { // replace saveme with an (unserialized) duplicate
|
||||
saveme = (NoteTrack *) this->Duplicate();
|
||||
assert(saveme->mSeq);
|
||||
}
|
||||
saveme->mSeq->write(data, true);
|
||||
xmlFile.StartTag(wxT("notetrack"));
|
||||
xmlFile.WriteAttr(wxT("name"), saveme->mName);
|
||||
xmlFile.WriteAttr(wxT("offset"), saveme->GetOffset());
|
||||
xmlFile.WriteAttr(wxT("visiblechannels"), saveme->mVisibleChannels);
|
||||
xmlFile.WriteAttr(wxT("height"), saveme->GetActualHeight());
|
||||
xmlFile.WriteAttr(wxT("minimized"), saveme->GetMinimized());
|
||||
xmlFile.WriteAttr(wxT("velocity"), (double) saveme->mGain);
|
||||
xmlFile.WriteAttr(wxT("bottomnote"), saveme->mBottomNote);
|
||||
xmlFile.WriteAttr(wxT("data"), data.str());
|
||||
xmlFile.EndTag(wxT("notetrack"));
|
||||
if (this != saveme) {
|
||||
delete saveme; // delete the duplicate
|
||||
}
|
||||
}
|
||||
|
||||
void NoteTrack::StartVScroll()
|
||||
{
|
||||
mStartBottomNote = mBottomNote;
|
||||
}
|
||||
|
||||
void NoteTrack::VScroll(int start, int end)
|
||||
{
|
||||
int ph = GetPitchHeight();
|
||||
int delta = ((end - start) + ph / 2) / ph;
|
||||
SetBottomNote(mStartBottomNote + delta);
|
||||
}
|
||||
|
||||
// Zoom the note track, centering the pitch at centerY,
|
||||
// amount is 1 for zoom in, and -1 for zoom out
|
||||
void NoteTrack::Zoom(int centerY, int amount)
|
||||
{
|
||||
// Construct track rectangle to map pitch to screen coordinates
|
||||
// Only y and height are needed:
|
||||
wxRect trackRect(0, GetY(), 1, GetHeight());
|
||||
PrepareIPitchToY(trackRect);
|
||||
int centerPitch = YToIPitch(centerY);
|
||||
// zoom out by changing the pitch height -- a small integer
|
||||
mPitchHeight += amount;
|
||||
if (mPitchHeight <= 0) mPitchHeight = 1;
|
||||
PrepareIPitchToY(trackRect); // update because mPitchHeight changed
|
||||
int newCenterPitch = YToIPitch(GetY() + GetHeight() / 2);
|
||||
// center the pitch that the user clicked on
|
||||
SetBottomNote(mBottomNote + (centerPitch - newCenterPitch));
|
||||
}
|
||||
|
||||
|
||||
void NoteTrack::ZoomTo(int start, int end)
|
||||
{
|
||||
wxRect trackRect(0, GetY(), 1, GetHeight());
|
||||
PrepareIPitchToY(trackRect);
|
||||
int topPitch = YToIPitch(start);
|
||||
int botPitch = YToIPitch(end);
|
||||
if (topPitch < botPitch) { // swap
|
||||
int temp = topPitch; topPitch = botPitch; botPitch = temp;
|
||||
}
|
||||
if (topPitch == botPitch) { // can't divide by zero, do something else
|
||||
Zoom(start, 1);
|
||||
return;
|
||||
}
|
||||
int trialPitchHeight = trackRect.height / (topPitch - botPitch);
|
||||
if (trialPitchHeight > 25) { // keep mPitchHeight in bounds [1...25]
|
||||
trialPitchHeight = 25;
|
||||
} else if (trialPitchHeight == 0) {
|
||||
trialPitchHeight = 1;
|
||||
}
|
||||
Zoom((start + end) / 2, trialPitchHeight - mPitchHeight);
|
||||
}
|
||||
|
||||
int NoteTrack::YToIPitch(int y)
|
||||
{
|
||||
y = mBottom - y; // pixels above pitch 0
|
||||
int octave = (y / GetOctaveHeight());
|
||||
y -= octave * GetOctaveHeight();
|
||||
// result is approximate because C and G are one pixel taller than
|
||||
// mPitchHeight.
|
||||
return (y / mPitchHeight) + octave * 12;
|
||||
}
|
||||
|
||||
#endif // USE_MIDI
|
||||
|
||||
Reference in New Issue
Block a user