summaryrefslogtreecommitdiffstats
path: root/arts/modules/synth/synth_midi_test_impl.cc
diff options
context:
space:
mode:
Diffstat (limited to 'arts/modules/synth/synth_midi_test_impl.cc')
-rw-r--r--arts/modules/synth/synth_midi_test_impl.cc692
1 files changed, 692 insertions, 0 deletions
diff --git a/arts/modules/synth/synth_midi_test_impl.cc b/arts/modules/synth/synth_midi_test_impl.cc
new file mode 100644
index 00000000..91714cac
--- /dev/null
+++ b/arts/modules/synth/synth_midi_test_impl.cc
@@ -0,0 +1,692 @@
+#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));
+ }
+}