You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
693 lines
15 KiB
693 lines
15 KiB
#include "artsmodulessynth.h"
|
|
#include "artsbuilder.h"
|
|
#include "stdsynthmodule.h"
|
|
#include "objectmanager.h"
|
|
#include "connect.h"
|
|
#include "flowsystem.h"
|
|
#include "debug.h"
|
|
#include "dynamicrequest.h"
|
|
#include "audiosubsys.h"
|
|
#include <fstream>
|
|
#include <math.h>
|
|
#include <stdlib.h>
|
|
|
|
using namespace Arts;
|
|
using namespace std;
|
|
|
|
/*-------- instrument mapping ---------*/
|
|
|
|
class InstrumentMap {
|
|
protected:
|
|
struct InstrumentData;
|
|
class Tokenizer;
|
|
list<InstrumentData> instruments;
|
|
string directory;
|
|
void loadLine(const string& line);
|
|
|
|
public:
|
|
struct InstrumentParam;
|
|
|
|
void loadFromList(const string& filename, const vector<string>& list);
|
|
StructureDesc getInstrument(mcopbyte channel, mcopbyte note,
|
|
mcopbyte velocity, mcopbyte program,
|
|
vector<InstrumentParam>*& params);
|
|
};
|
|
|
|
struct InstrumentMap::InstrumentParam
|
|
{
|
|
string param;
|
|
Any value;
|
|
|
|
InstrumentParam()
|
|
{
|
|
}
|
|
|
|
InstrumentParam(const InstrumentParam& src)
|
|
: param(src.param), value(src.value)
|
|
{
|
|
}
|
|
|
|
InstrumentParam(const string& param, const string& strValue)
|
|
: param(param)
|
|
{
|
|
/* put the string into the any */
|
|
value.type = "string";
|
|
|
|
Buffer b;
|
|
b.writeString(strValue);
|
|
b.read(value.value, b.size());
|
|
}
|
|
};
|
|
|
|
struct InstrumentMap::InstrumentData
|
|
{
|
|
struct Range
|
|
{
|
|
int minValue, maxValue;
|
|
Range() : minValue(0), maxValue(0)
|
|
{
|
|
}
|
|
Range(int minValue, int maxValue)
|
|
: minValue(minValue), maxValue(maxValue)
|
|
{
|
|
}
|
|
bool match(int value)
|
|
{
|
|
return (value >= minValue) && (value <= maxValue);
|
|
}
|
|
};
|
|
Range channel, pitch, program, velocity;
|
|
vector<InstrumentParam> params;
|
|
StructureDesc instrument;
|
|
};
|
|
|
|
class InstrumentMap::Tokenizer {
|
|
protected:
|
|
bool haveToken, haveNextToken;
|
|
string token, nextToken, input;
|
|
string::iterator ii;
|
|
public:
|
|
Tokenizer(const string& line)
|
|
: haveToken(false), haveNextToken(false),
|
|
input(line+"\n"), ii(input.begin())
|
|
{
|
|
/* adding a \n ensures that we will definitely find the last token */
|
|
}
|
|
string getToken()
|
|
{
|
|
if(!haveMore())
|
|
return "";
|
|
|
|
if(haveNextToken)
|
|
{
|
|
string t = token;
|
|
haveNextToken = false;
|
|
token = nextToken;
|
|
return t;
|
|
}
|
|
else
|
|
{
|
|
haveToken = false;
|
|
return token;
|
|
}
|
|
}
|
|
bool haveMore()
|
|
{
|
|
if(haveToken)
|
|
return true;
|
|
|
|
token = "";
|
|
while(ii != input.end() && !haveToken)
|
|
{
|
|
const char& c = *ii++;
|
|
|
|
if(c == ' ' || c == '\t' || c == '\n')
|
|
{
|
|
if(!token.empty()) haveToken = true;
|
|
}
|
|
else if(c == '=') /* || c == '-' || c == '+')*/
|
|
{
|
|
if(!token.empty())
|
|
{
|
|
haveNextToken = true;
|
|
nextToken = c;
|
|
}
|
|
else
|
|
{
|
|
token = c;
|
|
}
|
|
haveToken = true;
|
|
}
|
|
else
|
|
{
|
|
token += c;
|
|
}
|
|
}
|
|
return haveToken;
|
|
}
|
|
};
|
|
|
|
void InstrumentMap::loadLine(const string& line)
|
|
{
|
|
Tokenizer t(line);
|
|
InstrumentData id;
|
|
/* default: no filtering */
|
|
id.channel = InstrumentData::Range(0,15);
|
|
id.pitch = id.program = id.velocity = InstrumentData::Range(0,127);
|
|
|
|
string s[3];
|
|
int i = 0;
|
|
bool seenDo = false;
|
|
bool loadOk = false;
|
|
|
|
if(t.getToken() != "ON")
|
|
{
|
|
arts_warning("error in arts-map: lines must start with ON (did start with %s)\n", t.getToken().c_str());
|
|
return;
|
|
}
|
|
|
|
while(t.haveMore())
|
|
{
|
|
const string& token = t.getToken();
|
|
|
|
if(token == "DO")
|
|
seenDo = true;
|
|
else
|
|
{
|
|
s[i] = token;
|
|
if(i == 2) /* evaluate */
|
|
{
|
|
if(s[1] != "=")
|
|
{
|
|
arts_warning("error in arts-map: no = operator\n");
|
|
return;
|
|
}
|
|
|
|
if(seenDo)
|
|
{
|
|
if(s[0] == "structure")
|
|
{
|
|
string filename = s[2];
|
|
|
|
/* if it's no absolute path, its relative to the map */
|
|
if(!filename.empty() && filename[0] != '/')
|
|
filename = directory + "/" + s[2];
|
|
|
|
ifstream infile(filename.c_str());
|
|
string line;
|
|
vector<string> strseq;
|
|
|
|
while(getline(infile,line))
|
|
strseq.push_back(line);
|
|
|
|
id.instrument.loadFromList(strseq);
|
|
if(id.instrument.name() != "unknown")
|
|
{
|
|
loadOk = true;
|
|
}
|
|
else
|
|
{
|
|
arts_warning("mapped instrument: "
|
|
"can't load structure %s",s[2].c_str());
|
|
}
|
|
}
|
|
else
|
|
{
|
|
/* TODO: handle different datatypes */
|
|
id.params.push_back(InstrumentParam(s[0], s[2]));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
InstrumentData::Range range;
|
|
range.minValue = atoi(s[2].c_str());
|
|
range.maxValue = range.minValue;
|
|
int i = s[2].find("-",0);
|
|
if(i != 0)
|
|
{
|
|
range.minValue = atoi(s[2].substr(0,i).c_str());
|
|
range.maxValue =
|
|
atoi(s[2].substr(i+1,s[2].size()-(i+1)).c_str());
|
|
}
|
|
if(s[0] == "pitch") id.pitch = range;
|
|
if(s[0] == "channel") id.channel = range;
|
|
if(s[0] == "program") id.program = range;
|
|
if(s[0] == "velocity") id.velocity = range;
|
|
}
|
|
i = 0;
|
|
}
|
|
else i++;
|
|
}
|
|
}
|
|
if(loadOk) instruments.push_back(id);
|
|
}
|
|
|
|
void InstrumentMap::loadFromList(const string& filename,
|
|
const vector<string>& list)
|
|
{
|
|
int r = filename.rfind('/');
|
|
if(r > 0)
|
|
directory = filename.substr(0,r);
|
|
else
|
|
directory = "";
|
|
|
|
vector<string>::const_iterator i;
|
|
instruments.clear();
|
|
for(i = list.begin(); i != list.end(); i++) loadLine(*i);
|
|
}
|
|
|
|
StructureDesc InstrumentMap::getInstrument(mcopbyte channel, mcopbyte note,
|
|
mcopbyte velocity, mcopbyte program,
|
|
vector<InstrumentParam>*& params)
|
|
{
|
|
list<InstrumentData>::iterator i;
|
|
for(i = instruments.begin(); i != instruments.end(); i++)
|
|
{
|
|
InstrumentData &id = *i;
|
|
|
|
if(id.channel.match(channel) && id.pitch.match(note) &&
|
|
id.velocity.match(velocity) && id.program.match(program))
|
|
{
|
|
params = &id.params;
|
|
return id.instrument;
|
|
}
|
|
}
|
|
|
|
return StructureDesc::null();
|
|
}
|
|
|
|
|
|
/*-------instrument mapping end -------*/
|
|
|
|
static SynthModule get_AMAN_PLAY(Object structure)
|
|
{
|
|
Object resultObj = structure._getChild("play");
|
|
assert(!resultObj.isNull());
|
|
|
|
SynthModule result = DynamicCast(resultObj);
|
|
assert(!result.isNull());
|
|
|
|
return result;
|
|
}
|
|
|
|
struct TSNote {
|
|
MidiPort port;
|
|
MidiEvent event;
|
|
TSNote(MidiPort port, const MidiEvent& event) :
|
|
port(port), event(event)
|
|
{
|
|
}
|
|
};
|
|
|
|
class AutoMidiRelease : public TimeNotify {
|
|
public:
|
|
vector<MidiReleaseHelper> impls;
|
|
AutoMidiRelease()
|
|
{
|
|
Dispatcher::the()->ioManager()->addTimer(10, this);
|
|
}
|
|
virtual ~AutoMidiRelease()
|
|
{
|
|
Dispatcher::the()->ioManager()->removeTimer(this);
|
|
}
|
|
void notifyTime()
|
|
{
|
|
vector<MidiReleaseHelper>::iterator i = impls.begin();
|
|
while(i != impls.end())
|
|
{
|
|
if(i->terminate())
|
|
{
|
|
MidiReleaseHelper& helper = *i;
|
|
|
|
arts_debug("one voice terminated");
|
|
// put the MidiReleaseHelper and the voice into the ObjectCache
|
|
// (instead of simply freeing it)
|
|
ObjectCache cache = helper.cache();
|
|
SynthModule voice = helper.voice();
|
|
get_AMAN_PLAY(voice).stop();
|
|
voice.stop();
|
|
cache.put(voice,helper.name());
|
|
impls.erase(i);
|
|
return;
|
|
} else i++;
|
|
}
|
|
}
|
|
} *autoMidiRelease;
|
|
|
|
// cache startup & shutdown
|
|
static class AutoMidiReleaseStart :public StartupClass
|
|
{
|
|
public:
|
|
void startup() { autoMidiRelease = new AutoMidiRelease(); }
|
|
void shutdown() { delete autoMidiRelease; }
|
|
} autoMidiReleaseStart;
|
|
|
|
class MidiReleaseHelper_impl : virtual public MidiReleaseHelper_skel,
|
|
virtual public StdSynthModule
|
|
{
|
|
protected:
|
|
bool _terminate;
|
|
SynthModule _voice;
|
|
ObjectCache _cache;
|
|
string _name;
|
|
|
|
public:
|
|
MidiReleaseHelper_impl()
|
|
{
|
|
autoMidiRelease->impls.push_back(MidiReleaseHelper::_from_base(_copy()));
|
|
}
|
|
~MidiReleaseHelper_impl() {
|
|
artsdebug("MidiReleaseHelper: one voice is gone now\n");
|
|
}
|
|
|
|
|
|
SynthModule voice() { return _voice; }
|
|
void voice(SynthModule voice) { _voice = voice; }
|
|
|
|
ObjectCache cache() { return _cache; }
|
|
void cache(ObjectCache cache) { _cache = cache; }
|
|
|
|
string name() { return _name; }
|
|
void name(const string& name) { _name = name; }
|
|
|
|
bool terminate() { return _terminate; }
|
|
void streamStart() { _terminate = false; }
|
|
|
|
void calculateBlock(unsigned long /*samples*/)
|
|
{
|
|
if(done[0] > 0.5)
|
|
_terminate = true;
|
|
}
|
|
};
|
|
REGISTER_IMPLEMENTATION(MidiReleaseHelper_impl);
|
|
|
|
class Synth_MIDI_TEST_impl : virtual public Synth_MIDI_TEST_skel,
|
|
virtual public StdSynthModule {
|
|
protected:
|
|
struct ChannelData {
|
|
SynthModule voice[128];
|
|
string name[128];
|
|
float pitchShiftValue;
|
|
mcopbyte program;
|
|
ChannelData() {
|
|
// initialize all voices with NULL objects (no lazy create)
|
|
for(int i = 0; i < 128; i++) voice[i] = SynthModule::null();
|
|
|
|
pitchShiftValue = 0.0;
|
|
program = 0;
|
|
}
|
|
} *channelData; /* data for all 16 midi channels */
|
|
|
|
bool useMap;
|
|
InstrumentMap map;
|
|
StructureDesc instrument;
|
|
StructureBuilder builder;
|
|
AudioManagerClient amClient;
|
|
ObjectCache cache;
|
|
MidiClient client;
|
|
MidiTimer timer;
|
|
|
|
string _filename;
|
|
string _busname;
|
|
string _title;
|
|
public:
|
|
Synth_MIDI_TEST self() { return Synth_MIDI_TEST::_from_base(_copy()); }
|
|
|
|
Synth_MIDI_TEST_impl();
|
|
~Synth_MIDI_TEST_impl();
|
|
|
|
void filename(const string& newname);
|
|
string filename()
|
|
{
|
|
return _filename;
|
|
}
|
|
void busname(const string& newname);
|
|
string busname()
|
|
{
|
|
return _busname;
|
|
}
|
|
string title()
|
|
{
|
|
return _title;
|
|
}
|
|
void noteOn(mcopbyte channel, mcopbyte note, mcopbyte velocity);
|
|
void noteOff(mcopbyte channel, mcopbyte note);
|
|
void pitchWheel(mcopbyte channel, mcopbyte lsb, mcopbyte msb);
|
|
|
|
float getFrequency(mcopbyte note,mcopbyte channel);
|
|
|
|
void streamStart();
|
|
void streamEnd();
|
|
|
|
TimeStamp time()
|
|
{
|
|
return timer.time();
|
|
}
|
|
TimeStamp playTime()
|
|
{
|
|
/*
|
|
* what the user currently hears is exactly latencySec before our
|
|
* port timeStamp (as this is the size of the audio buffer)
|
|
*/
|
|
double latencySec = AudioSubSystem::the()->outputDelay();
|
|
TimeStamp t = time();
|
|
|
|
int sec = int(latencySec);
|
|
t.sec -= sec;
|
|
latencySec -= double(sec);
|
|
t.usec -= int(latencySec * 1000000.0);
|
|
|
|
if (t.usec < 0)
|
|
{
|
|
t.usec += 1000000;
|
|
t.sec -= 1;
|
|
}
|
|
|
|
arts_assert(t.usec >= 0 && t.usec < 1000000);
|
|
return t;
|
|
}
|
|
void processEvent(const MidiEvent& event)
|
|
{
|
|
timer.queueEvent(self(),event);
|
|
}
|
|
void processCommand(const MidiCommand& command)
|
|
{
|
|
mcopbyte channel = command.status & mcsChannelMask;
|
|
|
|
switch(command.status & mcsCommandMask)
|
|
{
|
|
case mcsNoteOn: noteOn(channel,command.data1,command.data2);
|
|
return;
|
|
case mcsNoteOff: noteOff(channel,command.data1);
|
|
return;
|
|
case mcsPitchWheel: pitchWheel(channel,command.data1,command.data2);
|
|
return;
|
|
case mcsProgram: channelData[channel].program = command.data1;
|
|
return;
|
|
case mcsParameter:
|
|
if(command.data1 == mcpAllNotesOff && command.data2 == 0)
|
|
for(mcopbyte note=0; note<128; note++)
|
|
noteOff(channel,note);
|
|
return;
|
|
}
|
|
}
|
|
};
|
|
REGISTER_IMPLEMENTATION(Synth_MIDI_TEST_impl);
|
|
|
|
|
|
void Synth_MIDI_TEST_impl::busname(const string& newname)
|
|
{
|
|
// TODO:
|
|
_busname = newname;
|
|
}
|
|
|
|
void Synth_MIDI_TEST_impl::filename(const string& newname)
|
|
{
|
|
ifstream infile(newname.c_str());
|
|
string line;
|
|
vector<string> strseq;
|
|
|
|
while(getline(infile,line))
|
|
strseq.push_back(line);
|
|
|
|
_filename = newname;
|
|
|
|
/* search extension */
|
|
string::const_reverse_iterator i;
|
|
string extension;
|
|
bool extensionok = false;
|
|
|
|
for(i = newname.rbegin(); i != newname.rend() && !extensionok; i++)
|
|
{
|
|
if(*i == '.')
|
|
extensionok = true;
|
|
else
|
|
extension.insert(extension.begin(), (char)tolower(*i));
|
|
}
|
|
|
|
if(extensionok && extension == "arts")
|
|
{
|
|
instrument.loadFromList(strseq);
|
|
_title = "aRts Instrument ("+instrument.name()+")";
|
|
useMap = false;
|
|
}
|
|
else if(extensionok && extension == "arts-map")
|
|
{
|
|
map.loadFromList(newname, strseq);
|
|
_title = "aRts Instrument (mapped)";
|
|
useMap = true;
|
|
}
|
|
|
|
if(!client.isNull())
|
|
client.title(title());
|
|
amClient.title(title());
|
|
}
|
|
|
|
Synth_MIDI_TEST_impl::Synth_MIDI_TEST_impl()
|
|
: amClient(amPlay, "aRts Instrument","Synth_MIDI_TEST")
|
|
{
|
|
useMap = false;
|
|
client = MidiClient::null();
|
|
timer = SubClass("Arts::AudioMidiTimer");
|
|
channelData = new ChannelData[16];
|
|
}
|
|
|
|
Synth_MIDI_TEST_impl::~Synth_MIDI_TEST_impl()
|
|
{
|
|
delete[] channelData;
|
|
}
|
|
|
|
void Synth_MIDI_TEST_impl::streamStart()
|
|
{
|
|
// register with the midi manager
|
|
MidiManager manager = Reference("global:Arts_MidiManager");
|
|
if(!manager.isNull())
|
|
{
|
|
client = manager.addClient(mcdRecord,mctDestination,title(),
|
|
"Arts::Synth_MIDI_TEST");
|
|
client.addInputPort(self());
|
|
}
|
|
else
|
|
arts_warning("Synth_MIDI_TEST: no midi manager found - not registered");
|
|
}
|
|
|
|
void Synth_MIDI_TEST_impl::streamEnd()
|
|
{
|
|
client = MidiClient::null();
|
|
}
|
|
|
|
void Synth_MIDI_TEST_impl::noteOn(mcopbyte channel, mcopbyte note,
|
|
mcopbyte velocity)
|
|
{
|
|
if(velocity == 0)
|
|
{
|
|
noteOff(channel,note);
|
|
return;
|
|
}
|
|
if(!channelData[channel].voice[note].isNull())
|
|
{
|
|
noteOff(channel,note);
|
|
arts_info("Synth_MIDI_TEST: duplicate noteOn (mixed channels?)");
|
|
}
|
|
|
|
vector<InstrumentMap::InstrumentParam> *params = 0;
|
|
if(useMap)
|
|
{
|
|
mcopbyte program = channelData[channel].program;
|
|
StructureDesc sd = map.getInstrument(channel,note,velocity,program,params);
|
|
if(sd.isNull()) return;
|
|
instrument = sd;
|
|
}
|
|
|
|
Object structureObject = cache.get(instrument.name());
|
|
if(structureObject.isNull())
|
|
{
|
|
arts_debug("creating new structure");
|
|
structureObject = builder.createObject(instrument);
|
|
|
|
SynthModule play;
|
|
// TODO: allow changing busname!
|
|
if(!_busname.empty())
|
|
{
|
|
Synth_BUS_UPLINK b;
|
|
b.busname(_busname);
|
|
play = b;
|
|
}
|
|
else
|
|
{
|
|
Synth_AMAN_PLAY a(amClient);
|
|
play = a;
|
|
}
|
|
structureObject._addChild(play,"play");
|
|
connect(structureObject,"left",play,"left");
|
|
connect(structureObject,"right",play,"right");
|
|
}
|
|
else
|
|
{
|
|
arts_debug("used cached structure");
|
|
}
|
|
|
|
SynthModule structure = DynamicCast(structureObject);
|
|
assert(!structure.isNull());
|
|
|
|
if(params)
|
|
{
|
|
vector<InstrumentMap::InstrumentParam>::iterator pi;
|
|
for(pi = params->begin(); pi != params->end(); pi++)
|
|
{
|
|
DynamicRequest req(structure);
|
|
|
|
req.method("_set_"+pi->param).param(pi->value).invoke();
|
|
}
|
|
}
|
|
setValue(structure,"frequency",getFrequency(note,channel));
|
|
setValue(structure,"velocity",(float)velocity/127.0);
|
|
setValue(structure,"pressed",1.0);
|
|
|
|
get_AMAN_PLAY(structure).start();
|
|
structure.start();
|
|
|
|
channelData[channel].voice[note] = structure;
|
|
channelData[channel].name[note] = instrument.name();
|
|
}
|
|
|
|
void Synth_MIDI_TEST_impl::noteOff(mcopbyte channel, mcopbyte note)
|
|
{
|
|
if(!channelData[channel].voice[note].isNull())
|
|
{
|
|
setValue(channelData[channel].voice[note],"pressed",0.0);
|
|
|
|
MidiReleaseHelper h;
|
|
|
|
h.voice(channelData[channel].voice[note]);
|
|
h.cache(cache);
|
|
h.name(channelData[channel].name[note]);
|
|
|
|
connect(channelData[channel].voice[note],"done",h,"done");
|
|
h.start();
|
|
assert(!h.terminate());
|
|
channelData[channel].voice[note] = SynthModule::null();
|
|
}
|
|
}
|
|
|
|
float Synth_MIDI_TEST_impl::getFrequency(mcopbyte note, mcopbyte channel)
|
|
{
|
|
/* 2 semitones pitchshift */
|
|
return 261.63 * pow(2,((float)(note)+(channelData[channel].pitchShiftValue*2.0))/12.0)/32.0;
|
|
}
|
|
|
|
void Synth_MIDI_TEST_impl::pitchWheel(mcopbyte channel,
|
|
mcopbyte lsb, mcopbyte msb)
|
|
{
|
|
mcopbyte note;
|
|
|
|
channelData[channel].pitchShiftValue =
|
|
(float)((lsb + msb*128) - (0x40*128))/8192.0;
|
|
|
|
for(note = 0; note < 128; note++)
|
|
{
|
|
if(!channelData[channel].voice[note].isNull())
|
|
setValue(channelData[channel].voice[note],"frequency",getFrequency(note,channel));
|
|
}
|
|
}
|