mirror of
https://github.com/ultimatepp/ultimatepp.git
synced 2026-05-15 14:16:07 -06:00
449 lines
7.8 KiB
C++
449 lines
7.8 KiB
C++
#include "Synth.h"
|
|
|
|
struct QSFStatus : Moveable<QSFStatus> {
|
|
double tempo = 120;
|
|
int sound = -1;
|
|
double duration = 1;
|
|
int shift = 24;
|
|
|
|
double volume = 1;
|
|
double volume_low = 0;
|
|
double volume_high = 1;
|
|
double volume_start_at = 0;
|
|
double volume_mul = 0;
|
|
|
|
double GetVolume(double at) const { return clamp(volume + (at - volume_start_at) * volume_mul, volume_low, volume_high); }
|
|
};
|
|
|
|
struct QSFParser : SoundSequence, QSFStatus {
|
|
int lineno;
|
|
String line;
|
|
const char *ptr;
|
|
VectorMap<String, String> macro;
|
|
VectorMap<int, String> errors;
|
|
|
|
double at = 0;
|
|
double at0 = 0;
|
|
double end = 0;
|
|
|
|
Vector<QSFStatus> stack;
|
|
Vector<double> at_stack;
|
|
|
|
void Error(const char *s);
|
|
void Spaces();
|
|
String ReadID();
|
|
void Expand();
|
|
|
|
void Sound(const char *s);
|
|
void Tone(int tone, double duration);
|
|
void Process();
|
|
|
|
void Parse(Stream& s);
|
|
|
|
QSFParser() {
|
|
Sound("130.81:L250V100R30::::");
|
|
}
|
|
};
|
|
|
|
void QSFParser::Error(const char *s)
|
|
{
|
|
errors.Add(lineno, s);
|
|
}
|
|
|
|
void QSFParser::Spaces()
|
|
{
|
|
while(IsSpace(*ptr)) ptr++;
|
|
}
|
|
|
|
String QSFParser::ReadID()
|
|
{
|
|
Spaces();
|
|
String id;
|
|
if(IsAlpha(*ptr) || *ptr == '_') {
|
|
const char *b = ptr;
|
|
while(IsAlNum(*ptr) || *ptr == '_')
|
|
ptr++;
|
|
id = String(b, ptr);
|
|
}
|
|
return id;
|
|
}
|
|
|
|
void QSFParser::Expand()
|
|
{
|
|
bool expanded;
|
|
auto Error = [&](const char *s) { errors.Add(lineno, s); };
|
|
Index<int> used_macro;
|
|
do {
|
|
expanded = false;
|
|
String r;
|
|
ptr = line;
|
|
Index<int> pm;
|
|
auto GetMacro = [&]()->String {
|
|
String id = ReadID();
|
|
if(id.GetCount()) {
|
|
int q = macro.Find(id);
|
|
if(q >= 0) {
|
|
if(used_macro.Find(q) < 0) {
|
|
pm.FindAdd(q);
|
|
expanded = true;
|
|
return macro[q];
|
|
}
|
|
else
|
|
Error("recursive macro " + id);
|
|
}
|
|
else
|
|
Error("uknown macro " + id);
|
|
}
|
|
else
|
|
Error("missing macro id");
|
|
return Null;
|
|
};
|
|
while(*ptr) {
|
|
if(*ptr == '$') {
|
|
ptr++;
|
|
Spaces();
|
|
if(IsDigit(*ptr)) {
|
|
int n = 0;
|
|
while(IsDigit(*ptr)) {
|
|
n = 10 * n + *ptr - '0';
|
|
ptr++;
|
|
}
|
|
Spaces();
|
|
String txt;
|
|
int lvl = 0;
|
|
auto Pars = [&](int l, int r) {
|
|
while(*ptr) {
|
|
if(*ptr == l)
|
|
lvl++;
|
|
if(*ptr == r && lvl-- == 0)
|
|
break;
|
|
ptr++;
|
|
}
|
|
};
|
|
if(*ptr == '[') {
|
|
const char *b = ptr++;
|
|
Pars('[', ']');
|
|
if(*ptr) ptr++;
|
|
txt = String(b, ptr);
|
|
}
|
|
else
|
|
if(*ptr == '(') {
|
|
const char *b = ++ptr;
|
|
Pars('(', ')');
|
|
txt = String(b, ptr);
|
|
if(*ptr) ptr++;
|
|
}
|
|
else
|
|
txt = GetMacro();
|
|
|
|
if(n > 0 && n < 100000) {
|
|
while(n--)
|
|
r << txt << ' ';
|
|
expanded = true;
|
|
}
|
|
else
|
|
Error("invalid repetition number");
|
|
}
|
|
else
|
|
r << GetMacro();
|
|
}
|
|
else
|
|
r << *ptr++;
|
|
}
|
|
line = r;
|
|
for(int q : pm)
|
|
used_macro.Add(q);
|
|
}
|
|
while(expanded);
|
|
}
|
|
|
|
void QSFParser::Parse(Stream& src)
|
|
{
|
|
while(!src.IsEof()) {
|
|
lineno++;
|
|
line = src.GetLine();
|
|
int q = line.Find("//");
|
|
if(q >= 0)
|
|
line.Trim(q);
|
|
line = TrimBoth(line);
|
|
if(*line == '#') {
|
|
ptr = ~line + 1;
|
|
String id = ReadID();
|
|
Spaces();
|
|
if(id.GetCount())
|
|
macro.GetAdd(id) = ptr;
|
|
else
|
|
Error("missing macro name");
|
|
}
|
|
else {
|
|
Expand();
|
|
Process();
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
void QSFParser::Sound(const char *s)
|
|
{
|
|
sound = SoundIndex(s);
|
|
}
|
|
|
|
void QSFParser::Tone(int tone, double duration)
|
|
{
|
|
duration = duration / tempo * 240;
|
|
if(tone == -1)
|
|
At(at);
|
|
else
|
|
Put(at, sound, 100 * GetVolume(at), 65.406 * exp2(tone / 12.0), 1000 * duration, IsNull(tone));
|
|
at += duration;
|
|
end = max(at, end);
|
|
}
|
|
|
|
void QSFParser::Process()
|
|
{
|
|
ptr = line;
|
|
auto ReadNumber = [&]() -> double {
|
|
CParser p(ptr);
|
|
p.NoSkipSpaces().NoSkipComments();
|
|
p.SkipSpaces();
|
|
if(p.IsDouble()) {
|
|
double h = p.ReadDouble();
|
|
ptr = p.GetPtr();
|
|
return h;
|
|
}
|
|
return 0;
|
|
};
|
|
auto ReadCNumber = [&]() -> double {
|
|
Spaces();
|
|
if(*ptr == ':')
|
|
ptr++;
|
|
return ReadNumber();
|
|
};
|
|
|
|
auto DoTone = [&](int tone) {
|
|
ptr++;
|
|
double ln = 1;
|
|
bool chrd = false;
|
|
for(;;) {
|
|
if(*ptr == '\'' || *ptr == '^')
|
|
tone += 12;
|
|
else
|
|
if(*ptr == ',')
|
|
tone -= 12;
|
|
else
|
|
if(*ptr == '=')
|
|
ln = ln + 1;
|
|
else
|
|
if(*ptr == '.')
|
|
ln = ln + 0.5;
|
|
else
|
|
if(*ptr == ':')
|
|
ln = ln + 0.75;
|
|
else
|
|
if(*ptr == '&')
|
|
chrd = true;
|
|
else
|
|
if(*ptr != ' ')
|
|
break;
|
|
ptr++;
|
|
}
|
|
double h = at;
|
|
Tone(shift + tone, ln * duration);
|
|
if(chrd)
|
|
at = h;
|
|
};
|
|
while(*ptr) {
|
|
if(*ptr == ';') {
|
|
ptr++;
|
|
if(*ptr == ';') {
|
|
at0 = at;
|
|
ptr++;
|
|
}
|
|
else
|
|
at = at0;
|
|
duration = 1;
|
|
shift = 24;
|
|
}
|
|
else
|
|
if(*ptr == '$') {
|
|
ptr++;
|
|
tempo = ReadNumber();
|
|
if(tempo < 1) {
|
|
Error("invalid tempo value");
|
|
tempo = 600;
|
|
}
|
|
}
|
|
else
|
|
if(*ptr == '+') {
|
|
ptr++;
|
|
shift += 12;
|
|
}
|
|
else
|
|
if(*ptr == '-') {
|
|
ptr++;
|
|
shift -= 12;
|
|
}
|
|
else
|
|
if(*ptr == '@') {
|
|
ptr++;
|
|
shift += (int)ReadNumber();
|
|
}
|
|
else
|
|
if(*ptr >= '0' && *ptr <= '9')
|
|
DoTone(*ptr - '0');
|
|
else
|
|
if(*ptr == 't')
|
|
DoTone(10);
|
|
else
|
|
if(*ptr == 'e')
|
|
DoTone(11);
|
|
else
|
|
if(*ptr == '_') {
|
|
Tone(-1, duration);
|
|
ptr++;
|
|
}
|
|
else
|
|
if(*ptr == '*') {
|
|
Tone(Null, duration);
|
|
ptr++;
|
|
}
|
|
else
|
|
if(*ptr == '/') {
|
|
ptr++;
|
|
duration = 1.0 / ReadNumber();
|
|
}
|
|
else
|
|
if(*ptr == 'q') {
|
|
ptr++;
|
|
duration = 1.0 / 4;
|
|
}
|
|
else
|
|
if(*ptr == 'o' || *ptr == 'w') {
|
|
ptr++;
|
|
duration = 1.0 / 8;
|
|
}
|
|
else
|
|
if(*ptr == 'x') {
|
|
ptr++;
|
|
duration = 1.0 / 16;
|
|
}
|
|
else
|
|
if(*ptr == 'y') {
|
|
ptr++;
|
|
duration = 1.0 / 32;
|
|
}
|
|
else
|
|
if(*ptr == 'm') {
|
|
ptr++;
|
|
duration = 1.0 / 2;
|
|
}
|
|
else
|
|
if(*ptr == 'n') {
|
|
ptr++;
|
|
duration = 1.0;
|
|
}
|
|
else
|
|
if(*ptr == '%') {
|
|
ptr++;
|
|
duration *= ReadNumber();
|
|
if(duration <= 0) {
|
|
Error("invalid tone length");
|
|
duration = 1;
|
|
}
|
|
}
|
|
else
|
|
if(*ptr == '!') {
|
|
ptr++;
|
|
String snd;
|
|
Spaces();
|
|
if(IsAlpha(*ptr)) {
|
|
String id = ReadID();
|
|
if(id == "loop")
|
|
LoopAt(at);
|
|
if(id == "shift")
|
|
shift += (int)ReadCNumber();
|
|
if(id == "volume") {
|
|
volume = ReadCNumber();
|
|
volume_mul = 0;
|
|
}
|
|
if(id == "volume_to") {
|
|
volume = GetVolume(at);
|
|
double v = ReadCNumber();
|
|
double len = ReadCNumber();
|
|
volume_low = min(v, volume);
|
|
volume_high = max(v, volume);
|
|
volume_start_at = at;
|
|
volume_mul = (v - volume) / (240 / tempo) / len;
|
|
}
|
|
if(id == "cursor")
|
|
SoundSequence::cursor = GetAt(at);
|
|
if(id == "cut") {
|
|
event.Trim(GetAt(at));
|
|
end = at;
|
|
}
|
|
if(id == "tempo") {
|
|
double t = ReadCNumber();
|
|
if(t < 1 || t > 10000) {
|
|
Error("invalid tempo value");
|
|
}
|
|
else
|
|
tempo = t;
|
|
}
|
|
}
|
|
else {
|
|
while(*ptr && *ptr != ' ')
|
|
snd.Cat(*ptr++);
|
|
Sound(snd);
|
|
}
|
|
}
|
|
else
|
|
if(*ptr == '[') {
|
|
ptr++;
|
|
if(stack.GetCount() < 100)
|
|
stack.Add() = *this;
|
|
else
|
|
Error("stack full");
|
|
}
|
|
else
|
|
if(*ptr == ']') {
|
|
ptr++;
|
|
if(stack.GetCount())
|
|
(QSFStatus&)*this = stack.Pop();
|
|
else
|
|
Error("stack empty");
|
|
}
|
|
else
|
|
if(*ptr == '{') {
|
|
ptr++;
|
|
if(stack.GetCount() < 100) {
|
|
stack.Add() = *this;
|
|
at_stack.Add(at);
|
|
}
|
|
else
|
|
Error("stack full");
|
|
}
|
|
else
|
|
if(*ptr == '}') {
|
|
ptr++;
|
|
if(stack.GetCount() && at_stack.GetCount()) {
|
|
(QSFStatus&)*this = stack.Pop();
|
|
at = at_stack.Pop();
|
|
}
|
|
else
|
|
Error("stack empty");
|
|
}
|
|
else
|
|
ptr++;
|
|
}
|
|
int q = GetAt(end);
|
|
if(q > 0)
|
|
event.At(q);
|
|
}
|
|
|
|
SoundSequence ParseQSF(const String& data)
|
|
{
|
|
StringStream ss(data);
|
|
QSFParser p;
|
|
p.Parse(ss);
|
|
return pick(p);
|
|
}
|