0) {
+ if(p < this.DB && (d = this[i]>>p) > 0) { m = true; r = int2char(d); }
+ while(i >= 0) {
+ if(p < k) {
+ d = (this[i]&((1<>(p+=this.DB-k);
+ }
+ else {
+ d = (this[i]>>(p-=k))&km;
+ if(p <= 0) { p += this.DB; --i; }
+ }
+ if(d > 0) m = true;
+ if(m) r += int2char(d);
+ }
+ }
+ return m?r:"0";
+ }
+
+ // (public) -this
+ function bnNegate() { var r = nbi(); BigInteger.ZERO.subTo(this,r); return r; }
+
+ // (public) |this|
+ function bnAbs() { return (this.s<0)?this.negate():this; }
+
+ // (public) return + if this > a, - if this < a, 0 if equal
+ function bnCompareTo(a) {
+ var r = this.s-a.s;
+ if(r != 0) return r;
+ var i = this.t;
+ r = i-a.t;
+ if(r != 0) return (this.s<0)?-r:r;
+ while(--i >= 0) if((r=this[i]-a[i]) != 0) return r;
+ return 0;
+ }
+
+ // returns bit length of the integer x
+ function nbits(x) {
+ var r = 1, t;
+ if((t=x>>>16) != 0) { x = t; r += 16; }
+ if((t=x>>8) != 0) { x = t; r += 8; }
+ if((t=x>>4) != 0) { x = t; r += 4; }
+ if((t=x>>2) != 0) { x = t; r += 2; }
+ if((t=x>>1) != 0) { x = t; r += 1; }
+ return r;
+ }
+
+ // (public) return the number of bits in "this"
+ function bnBitLength() {
+ if(this.t <= 0) return 0;
+ return this.DB*(this.t-1)+nbits(this[this.t-1]^(this.s&this.DM));
+ }
+
+ // (protected) r = this << n*DB
+ function bnpDLShiftTo(n,r) {
+ var i;
+ for(i = this.t-1; i >= 0; --i) r[i+n] = this[i];
+ for(i = n-1; i >= 0; --i) r[i] = 0;
+ r.t = this.t+n;
+ r.s = this.s;
+ }
+
+ // (protected) r = this >> n*DB
+ function bnpDRShiftTo(n,r) {
+ for(var i = n; i < this.t; ++i) r[i-n] = this[i];
+ r.t = Math.max(this.t-n,0);
+ r.s = this.s;
+ }
+
+ // (protected) r = this << n
+ function bnpLShiftTo(n,r) {
+ var bs = n%this.DB;
+ var cbs = this.DB-bs;
+ var bm = (1<= 0; --i) {
+ r[i+ds+1] = (this[i]>>cbs)|c;
+ c = (this[i]&bm)<= 0; --i) r[i] = 0;
+ r[ds] = c;
+ r.t = this.t+ds+1;
+ r.s = this.s;
+ r.clamp();
+ }
+
+ // (protected) r = this >> n
+ function bnpRShiftTo(n,r) {
+ r.s = this.s;
+ var ds = Math.floor(n/this.DB);
+ if(ds >= this.t) { r.t = 0; return; }
+ var bs = n%this.DB;
+ var cbs = this.DB-bs;
+ var bm = (1<>bs;
+ for(var i = ds+1; i < this.t; ++i) {
+ r[i-ds-1] |= (this[i]&bm)<>bs;
+ }
+ if(bs > 0) r[this.t-ds-1] |= (this.s&bm)<>= this.DB;
+ }
+ if(a.t < this.t) {
+ c -= a.s;
+ while(i < this.t) {
+ c += this[i];
+ r[i++] = c&this.DM;
+ c >>= this.DB;
+ }
+ c += this.s;
+ }
+ else {
+ c += this.s;
+ while(i < a.t) {
+ c -= a[i];
+ r[i++] = c&this.DM;
+ c >>= this.DB;
+ }
+ c -= a.s;
+ }
+ r.s = (c<0)?-1:0;
+ if(c < -1) r[i++] = this.DV+c;
+ else if(c > 0) r[i++] = c;
+ r.t = i;
+ r.clamp();
+ }
+
+ // (protected) r = this * a, r != this,a (HAC 14.12)
+ // "this" should be the larger one if appropriate.
+ function bnpMultiplyTo(a,r) {
+ var x = this.abs(), y = a.abs();
+ var i = x.t;
+ r.t = i+y.t;
+ while(--i >= 0) r[i] = 0;
+ for(i = 0; i < y.t; ++i) r[i+x.t] = x.am(0,y[i],r,i,0,x.t);
+ r.s = 0;
+ r.clamp();
+ if(this.s != a.s) BigInteger.ZERO.subTo(r,r);
+ }
+
+ // (protected) r = this^2, r != this (HAC 14.16)
+ function bnpSquareTo(r) {
+ var x = this.abs();
+ var i = r.t = 2*x.t;
+ while(--i >= 0) r[i] = 0;
+ for(i = 0; i < x.t-1; ++i) {
+ var c = x.am(i,x[i],r,2*i,0,1);
+ if((r[i+x.t]+=x.am(i+1,2*x[i],r,2*i+1,c,x.t-i-1)) >= x.DV) {
+ r[i+x.t] -= x.DV;
+ r[i+x.t+1] = 1;
+ }
+ }
+ if(r.t > 0) r[r.t-1] += x.am(i,x[i],r,2*i,0,1);
+ r.s = 0;
+ r.clamp();
+ }
+
+ // (protected) divide this by m, quotient and remainder to q, r (HAC 14.20)
+ // r != q, this != m. q or r may be null.
+ function bnpDivRemTo(m,q,r) {
+ var pm = m.abs();
+ if(pm.t <= 0) return;
+ var pt = this.abs();
+ if(pt.t < pm.t) {
+ if(q != null) q.fromInt(0);
+ if(r != null) this.copyTo(r);
+ return;
+ }
+ if(r == null) r = nbi();
+ var y = nbi(), ts = this.s, ms = m.s;
+ var nsh = this.DB-nbits(pm[pm.t-1]); // normalize modulus
+ if(nsh > 0) { pm.lShiftTo(nsh,y); pt.lShiftTo(nsh,r); }
+ else { pm.copyTo(y); pt.copyTo(r); }
+ var ys = y.t;
+ var y0 = y[ys-1];
+ if(y0 == 0) return;
+ var yt = y0*(1<1)?y[ys-2]>>this.F2:0);
+ var d1 = this.FV/yt, d2 = (1<= 0) {
+ r[r.t++] = 1;
+ r.subTo(t,r);
+ }
+ BigInteger.ONE.dlShiftTo(ys,t);
+ t.subTo(y,y); // "negative" y so we can replace sub with am later
+ while(y.t < ys) y[y.t++] = 0;
+ while(--j >= 0) {
+ // Estimate quotient digit
+ var qd = (r[--i]==y0)?this.DM:Math.floor(r[i]*d1+(r[i-1]+e)*d2);
+ if((r[i]+=y.am(0,qd,r,j,0,ys)) < qd) { // Try it out
+ y.dlShiftTo(j,t);
+ r.subTo(t,r);
+ while(r[i] < --qd) r.subTo(t,r);
+ }
+ }
+ if(q != null) {
+ r.drShiftTo(ys,q);
+ if(ts != ms) BigInteger.ZERO.subTo(q,q);
+ }
+ r.t = ys;
+ r.clamp();
+ if(nsh > 0) r.rShiftTo(nsh,r); // Denormalize remainder
+ if(ts < 0) BigInteger.ZERO.subTo(r,r);
+ }
+
+ // (public) this mod a
+ function bnMod(a) {
+ var r = nbi();
+ this.abs().divRemTo(a,null,r);
+ if(this.s < 0 && r.compareTo(BigInteger.ZERO) > 0) a.subTo(r,r);
+ return r;
+ }
+
+ // Modular reduction using "classic" algorithm
+ function Classic(m) { this.m = m; }
+ function cConvert(x) {
+ if(x.s < 0 || x.compareTo(this.m) >= 0) return x.mod(this.m);
+ else return x;
+ }
+ function cRevert(x) { return x; }
+ function cReduce(x) { x.divRemTo(this.m,null,x); }
+ function cMulTo(x,y,r) { x.multiplyTo(y,r); this.reduce(r); }
+ function cSqrTo(x,r) { x.squareTo(r); this.reduce(r); }
+
+ Classic.prototype.convert = cConvert;
+ Classic.prototype.revert = cRevert;
+ Classic.prototype.reduce = cReduce;
+ Classic.prototype.mulTo = cMulTo;
+ Classic.prototype.sqrTo = cSqrTo;
+
+ // (protected) return "-1/this % 2^DB"; useful for Mont. reduction
+ // justification:
+ // xy == 1 (mod m)
+ // xy = 1+km
+ // xy(2-xy) = (1+km)(1-km)
+ // x[y(2-xy)] = 1-k^2m^2
+ // x[y(2-xy)] == 1 (mod m^2)
+ // if y is 1/x mod m, then y(2-xy) is 1/x mod m^2
+ // should reduce x and y(2-xy) by m^2 at each step to keep size bounded.
+ // JS multiply "overflows" differently from C/C++, so care is needed here.
+ function bnpInvDigit() {
+ if(this.t < 1) return 0;
+ var x = this[0];
+ if((x&1) == 0) return 0;
+ var y = x&3; // y == 1/x mod 2^2
+ y = (y*(2-(x&0xf)*y))&0xf; // y == 1/x mod 2^4
+ y = (y*(2-(x&0xff)*y))&0xff; // y == 1/x mod 2^8
+ y = (y*(2-(((x&0xffff)*y)&0xffff)))&0xffff; // y == 1/x mod 2^16
+ // last step - calculate inverse mod DV directly;
+ // assumes 16 < DB <= 32 and assumes ability to handle 48-bit ints
+ y = (y*(2-x*y%this.DV))%this.DV; // y == 1/x mod 2^dbits
+ // we really want the negative inverse, and -DV < y < DV
+ return (y>0)?this.DV-y:-y;
+ }
+
+ // Montgomery reduction
+ function Montgomery(m) {
+ this.m = m;
+ this.mp = m.invDigit();
+ this.mpl = this.mp&0x7fff;
+ this.mph = this.mp>>15;
+ this.um = (1<<(m.DB-15))-1;
+ this.mt2 = 2*m.t;
+ }
+
+ // xR mod m
+ function montConvert(x) {
+ var r = nbi();
+ x.abs().dlShiftTo(this.m.t,r);
+ r.divRemTo(this.m,null,r);
+ if(x.s < 0 && r.compareTo(BigInteger.ZERO) > 0) this.m.subTo(r,r);
+ return r;
+ }
+
+ // x/R mod m
+ function montRevert(x) {
+ var r = nbi();
+ x.copyTo(r);
+ this.reduce(r);
+ return r;
+ }
+
+ // x = x/R mod m (HAC 14.32)
+ function montReduce(x) {
+ while(x.t <= this.mt2) // pad x so am has enough room later
+ x[x.t++] = 0;
+ for(var i = 0; i < this.m.t; ++i) {
+ // faster way of calculating u0 = x[i]*mp mod DV
+ var j = x[i]&0x7fff;
+ var u0 = (j*this.mpl+(((j*this.mph+(x[i]>>15)*this.mpl)&this.um)<<15))&x.DM;
+ // use am to combine the multiply-shift-add into one call
+ j = i+this.m.t;
+ x[j] += this.m.am(0,u0,x,i,0,this.m.t);
+ // propagate carry
+ while(x[j] >= x.DV) { x[j] -= x.DV; x[++j]++; }
+ }
+ x.clamp();
+ x.drShiftTo(this.m.t,x);
+ if(x.compareTo(this.m) >= 0) x.subTo(this.m,x);
+ }
+
+ // r = "x^2/R mod m"; x != r
+ function montSqrTo(x,r) { x.squareTo(r); this.reduce(r); }
+
+ // r = "xy/R mod m"; x,y != r
+ function montMulTo(x,y,r) { x.multiplyTo(y,r); this.reduce(r); }
+
+ Montgomery.prototype.convert = montConvert;
+ Montgomery.prototype.revert = montRevert;
+ Montgomery.prototype.reduce = montReduce;
+ Montgomery.prototype.mulTo = montMulTo;
+ Montgomery.prototype.sqrTo = montSqrTo;
+
+ // (protected) true iff this is even
+ function bnpIsEven() { return ((this.t>0)?(this[0]&1):this.s) == 0; }
+
+ // (protected) this^e, e < 2^32, doing sqr and mul with "r" (HAC 14.79)
+ function bnpExp(e,z) {
+ if(e > 0xffffffff || e < 1) return BigInteger.ONE;
+ var r = nbi(), r2 = nbi(), g = z.convert(this), i = nbits(e)-1;
+ g.copyTo(r);
+ while(--i >= 0) {
+ z.sqrTo(r,r2);
+ if((e&(1< 0) z.mulTo(r2,g,r);
+ else { var t = r; r = r2; r2 = t; }
+ }
+ return z.revert(r);
+ }
+
+ // (public) this^e % m, 0 <= e < 2^32
+ function bnModPowInt(e,m) {
+ var z;
+ if(e < 256 || m.isEven()) z = new Classic(m); else z = new Montgomery(m);
+ return this.exp(e,z);
+ }
+
+ // protected
+ BigInteger.prototype.copyTo = bnpCopyTo;
+ BigInteger.prototype.fromInt = bnpFromInt;
+ BigInteger.prototype.fromString = bnpFromString;
+ BigInteger.prototype.clamp = bnpClamp;
+ BigInteger.prototype.dlShiftTo = bnpDLShiftTo;
+ BigInteger.prototype.drShiftTo = bnpDRShiftTo;
+ BigInteger.prototype.lShiftTo = bnpLShiftTo;
+ BigInteger.prototype.rShiftTo = bnpRShiftTo;
+ BigInteger.prototype.subTo = bnpSubTo;
+ BigInteger.prototype.multiplyTo = bnpMultiplyTo;
+ BigInteger.prototype.squareTo = bnpSquareTo;
+ BigInteger.prototype.divRemTo = bnpDivRemTo;
+ BigInteger.prototype.invDigit = bnpInvDigit;
+ BigInteger.prototype.isEven = bnpIsEven;
+ BigInteger.prototype.exp = bnpExp;
+
+ // public
+ BigInteger.prototype.toString = bnToString;
+ BigInteger.prototype.negate = bnNegate;
+ BigInteger.prototype.abs = bnAbs;
+ BigInteger.prototype.compareTo = bnCompareTo;
+ BigInteger.prototype.bitLength = bnBitLength;
+ BigInteger.prototype.mod = bnMod;
+ BigInteger.prototype.modPowInt = bnModPowInt;
+
+ // "constants"
+ BigInteger.ZERO = nbv(0);
+ BigInteger.ONE = nbv(1);
+
+ // jsbn2 stuff
+
+ // (protected) convert from radix string
+ function bnpFromRadix(s,b) {
+ this.fromInt(0);
+ if(b == null) b = 10;
+ var cs = this.chunkSize(b);
+ var d = Math.pow(b,cs), mi = false, j = 0, w = 0;
+ for(var i = 0; i < s.length; ++i) {
+ var x = intAt(s,i);
+ if(x < 0) {
+ if(s.charAt(i) == "-" && this.signum() == 0) mi = true;
+ continue;
+ }
+ w = b*w+x;
+ if(++j >= cs) {
+ this.dMultiply(d);
+ this.dAddOffset(w,0);
+ j = 0;
+ w = 0;
+ }
+ }
+ if(j > 0) {
+ this.dMultiply(Math.pow(b,j));
+ this.dAddOffset(w,0);
+ }
+ if(mi) BigInteger.ZERO.subTo(this,this);
+ }
+
+ // (protected) return x s.t. r^x < DV
+ function bnpChunkSize(r) { return Math.floor(Math.LN2*this.DB/Math.log(r)); }
+
+ // (public) 0 if this == 0, 1 if this > 0
+ function bnSigNum() {
+ if(this.s < 0) return -1;
+ else if(this.t <= 0 || (this.t == 1 && this[0] <= 0)) return 0;
+ else return 1;
+ }
+
+ // (protected) this *= n, this >= 0, 1 < n < DV
+ function bnpDMultiply(n) {
+ this[this.t] = this.am(0,n-1,this,0,0,this.t);
+ ++this.t;
+ this.clamp();
+ }
+
+ // (protected) this += n << w words, this >= 0
+ function bnpDAddOffset(n,w) {
+ if(n == 0) return;
+ while(this.t <= w) this[this.t++] = 0;
+ this[w] += n;
+ while(this[w] >= this.DV) {
+ this[w] -= this.DV;
+ if(++w >= this.t) this[this.t++] = 0;
+ ++this[w];
+ }
+ }
+
+ // (protected) convert to radix string
+ function bnpToRadix(b) {
+ if(b == null) b = 10;
+ if(this.signum() == 0 || b < 2 || b > 36) return "0";
+ var cs = this.chunkSize(b);
+ var a = Math.pow(b,cs);
+ var d = nbv(a), y = nbi(), z = nbi(), r = "";
+ this.divRemTo(d,y,z);
+ while(y.signum() > 0) {
+ r = (a+z.intValue()).toString(b).substr(1) + r;
+ y.divRemTo(d,y,z);
+ }
+ return z.intValue().toString(b) + r;
+ }
+
+ // (public) return value as integer
+ function bnIntValue() {
+ if(this.s < 0) {
+ if(this.t == 1) return this[0]-this.DV;
+ else if(this.t == 0) return -1;
+ }
+ else if(this.t == 1) return this[0];
+ else if(this.t == 0) return 0;
+ // assumes 16 < DB < 32
+ return ((this[1]&((1<<(32-this.DB))-1))<>= this.DB;
+ }
+ if(a.t < this.t) {
+ c += a.s;
+ while(i < this.t) {
+ c += this[i];
+ r[i++] = c&this.DM;
+ c >>= this.DB;
+ }
+ c += this.s;
+ }
+ else {
+ c += this.s;
+ while(i < a.t) {
+ c += a[i];
+ r[i++] = c&this.DM;
+ c >>= this.DB;
+ }
+ c += a.s;
+ }
+ r.s = (c<0)?-1:0;
+ if(c > 0) r[i++] = c;
+ else if(c < -1) r[i++] = this.DV+c;
+ r.t = i;
+ r.clamp();
+ }
+
+ BigInteger.prototype.fromRadix = bnpFromRadix;
+ BigInteger.prototype.chunkSize = bnpChunkSize;
+ BigInteger.prototype.signum = bnSigNum;
+ BigInteger.prototype.dMultiply = bnpDMultiply;
+ BigInteger.prototype.dAddOffset = bnpDAddOffset;
+ BigInteger.prototype.toRadix = bnpToRadix;
+ BigInteger.prototype.intValue = bnIntValue;
+ BigInteger.prototype.addTo = bnpAddTo;
+
+ //======= end jsbn =======
+
+ // Emscripten wrapper
+ var Wrapper = {
+ abs: function(l, h) {
+ var x = new goog.math.Long(l, h);
+ var ret;
+ if (x.isNegative()) {
+ ret = x.negate();
+ } else {
+ ret = x;
+ }
+ HEAP32[tempDoublePtr>>2] = ret.low_;
+ HEAP32[tempDoublePtr+4>>2] = ret.high_;
+ },
+ ensureTemps: function() {
+ if (Wrapper.ensuredTemps) return;
+ Wrapper.ensuredTemps = true;
+ Wrapper.two32 = new BigInteger();
+ Wrapper.two32.fromString('4294967296', 10);
+ Wrapper.two64 = new BigInteger();
+ Wrapper.two64.fromString('18446744073709551616', 10);
+ Wrapper.temp1 = new BigInteger();
+ Wrapper.temp2 = new BigInteger();
+ },
+ lh2bignum: function(l, h) {
+ var a = new BigInteger();
+ a.fromString(h.toString(), 10);
+ var b = new BigInteger();
+ a.multiplyTo(Wrapper.two32, b);
+ var c = new BigInteger();
+ c.fromString(l.toString(), 10);
+ var d = new BigInteger();
+ c.addTo(b, d);
+ return d;
+ },
+ stringify: function(l, h, unsigned) {
+ var ret = new goog.math.Long(l, h).toString();
+ if (unsigned && ret[0] == '-') {
+ // unsign slowly using jsbn bignums
+ Wrapper.ensureTemps();
+ var bignum = new BigInteger();
+ bignum.fromString(ret, 10);
+ ret = new BigInteger();
+ Wrapper.two64.addTo(bignum, ret);
+ ret = ret.toString(10);
+ }
+ return ret;
+ },
+ fromString: function(str, base, min, max, unsigned) {
+ Wrapper.ensureTemps();
+ var bignum = new BigInteger();
+ bignum.fromString(str, base);
+ var bigmin = new BigInteger();
+ bigmin.fromString(min, 10);
+ var bigmax = new BigInteger();
+ bigmax.fromString(max, 10);
+ if (unsigned && bignum.compareTo(BigInteger.ZERO) < 0) {
+ var temp = new BigInteger();
+ bignum.addTo(Wrapper.two64, temp);
+ bignum = temp;
+ }
+ var error = false;
+ if (bignum.compareTo(bigmin) < 0) {
+ bignum = bigmin;
+ error = true;
+ } else if (bignum.compareTo(bigmax) > 0) {
+ bignum = bigmax;
+ error = true;
+ }
+ var ret = goog.math.Long.fromString(bignum.toString()); // min-max checks should have clamped this to a range goog.math.Long can handle well
+ HEAP32[tempDoublePtr>>2] = ret.low_;
+ HEAP32[tempDoublePtr+4>>2] = ret.high_;
+ if (error) throw 'range error';
+ }
+ };
+ return Wrapper;
+})();
+
+//======= end closure i64 code =======
+
+
+
+// === Auto-generated postamble setup entry stuff ===
+
+
+function ExitStatus(status) {
+ this.name = "ExitStatus";
+ this.message = "Program terminated with exit(" + status + ")";
+ this.status = status;
+};
+ExitStatus.prototype = new Error();
+ExitStatus.prototype.constructor = ExitStatus;
+
+var initialStackTop;
+var preloadStartTime = null;
+var calledMain = false;
+
+dependenciesFulfilled = function runCaller() {
+ // If run has never been called, and we should call run (INVOKE_RUN is true, and Module.noInitialRun is not false)
+ if (!Module['calledRun']) run();
+ if (!Module['calledRun']) dependenciesFulfilled = runCaller; // try this again later, after new deps are fulfilled
+}
+
+Module['callMain'] = Module.callMain = function callMain(args) {
+ assert(runDependencies == 0, 'cannot call main when async dependencies remain! (listen on __ATMAIN__)');
+ assert(__ATPRERUN__.length == 0, 'cannot call main when preRun functions remain to be called');
+
+ args = args || [];
+
+ ensureInitRuntime();
+
+ var argc = args.length+1;
+ function pad() {
+ for (var i = 0; i < 4-1; i++) {
+ argv.push(0);
+ }
+ }
+ var argv = [allocate(intArrayFromString(Module['thisProgram']), 'i8', ALLOC_NORMAL) ];
+ pad();
+ for (var i = 0; i < argc-1; i = i + 1) {
+ argv.push(allocate(intArrayFromString(args[i]), 'i8', ALLOC_NORMAL));
+ pad();
+ }
+ argv.push(0);
+ argv = allocate(argv, 'i32', ALLOC_NORMAL);
+
+ initialStackTop = STACKTOP;
+
+ try {
+
+ var ret = Module['_main'](argc, argv, 0);
+
+
+ // if we're not running an evented main loop, it's time to exit
+ exit(ret, /* implicit = */ true);
+ }
+ catch(e) {
+ if (e instanceof ExitStatus) {
+ // exit() throws this once it's done to make sure execution
+ // has been stopped completely
+ return;
+ } else if (e == 'SimulateInfiniteLoop') {
+ // running an evented main loop, don't immediately exit
+ Module['noExitRuntime'] = true;
+ return;
+ } else {
+ if (e && typeof e === 'object' && e.stack) Module.printErr('exception thrown: ' + [e, e.stack]);
+ throw e;
+ }
+ } finally {
+ calledMain = true;
+ }
+}
+
+
+
+
+function run(args) {
+ args = args || Module['arguments'];
+
+ if (preloadStartTime === null) preloadStartTime = Date.now();
+
+ if (runDependencies > 0) {
+ return;
+ }
+
+ preRun();
+
+ if (runDependencies > 0) return; // a preRun added a dependency, run will be called later
+ if (Module['calledRun']) return; // run may have just been called through dependencies being fulfilled just in this very frame
+
+ function doRun() {
+ if (Module['calledRun']) return; // run may have just been called while the async setStatus time below was happening
+ Module['calledRun'] = true;
+
+ if (ABORT) return;
+
+ ensureInitRuntime();
+
+ preMain();
+
+ if (ENVIRONMENT_IS_WEB && preloadStartTime !== null) {
+ Module.printErr('pre-main prep time: ' + (Date.now() - preloadStartTime) + ' ms');
+ }
+
+ if (Module['onRuntimeInitialized']) Module['onRuntimeInitialized']();
+
+ if (Module['_main'] && shouldRunNow) Module['callMain'](args);
+
+ postRun();
+ }
+
+ if (Module['setStatus']) {
+ Module['setStatus']('Running...');
+ setTimeout(function() {
+ setTimeout(function() {
+ Module['setStatus']('');
+ }, 1);
+ doRun();
+ }, 1);
+ } else {
+ doRun();
+ }
+}
+Module['run'] = Module.run = run;
+
+function exit(status, implicit) {
+ if (implicit && Module['noExitRuntime']) {
+ return;
+ }
+
+ if (Module['noExitRuntime']) {
+ } else {
+
+ ABORT = true;
+ EXITSTATUS = status;
+ STACKTOP = initialStackTop;
+
+ exitRuntime();
+
+ if (Module['onExit']) Module['onExit'](status);
+ }
+
+ if (ENVIRONMENT_IS_NODE) {
+ // Work around a node.js bug where stdout buffer is not flushed at process exit:
+ // Instead of process.exit() directly, wait for stdout flush event.
+ // See https://github.com/joyent/node/issues/1669 and https://github.com/kripken/emscripten/issues/2582
+ // Workaround is based on https://github.com/RReverser/acorn/commit/50ab143cecc9ed71a2d66f78b4aec3bb2e9844f6
+ process['stdout']['once']('drain', function () {
+ process['exit'](status);
+ });
+ console.log(' '); // Make sure to print something to force the drain event to occur, in case the stdout buffer was empty.
+ // Work around another node bug where sometimes 'drain' is never fired - make another effort
+ // to emit the exit status, after a significant delay (if node hasn't fired drain by then, give up)
+ setTimeout(function() {
+ process['exit'](status);
+ }, 500);
+ } else
+ if (ENVIRONMENT_IS_SHELL && typeof quit === 'function') {
+ quit(status);
+ }
+ // if we reach here, we must throw an exception to halt the current execution
+ throw new ExitStatus(status);
+}
+Module['exit'] = Module.exit = exit;
+
+var abortDecorators = [];
+
+function abort(what) {
+ if (what !== undefined) {
+ Module.print(what);
+ Module.printErr(what);
+ what = JSON.stringify(what)
+ } else {
+ what = '';
+ }
+
+ ABORT = true;
+ EXITSTATUS = 1;
+
+ var extra = '\nIf this abort() is unexpected, build with -s ASSERTIONS=1 which can give more information.';
+
+ var output = 'abort(' + what + ') at ' + stackTrace() + extra;
+ if (abortDecorators) {
+ abortDecorators.forEach(function(decorator) {
+ output = decorator(output, what);
+ });
+ }
+ throw output;
+}
+Module['abort'] = Module.abort = abort;
+
+// {{PRE_RUN_ADDITIONS}}
+
+if (Module['preInit']) {
+ if (typeof Module['preInit'] == 'function') Module['preInit'] = [Module['preInit']];
+ while (Module['preInit'].length > 0) {
+ Module['preInit'].pop()();
+ }
+}
+
+// shouldRunNow refers to calling main(), not run().
+var shouldRunNow = true;
+if (Module['noInitialRun']) {
+ shouldRunNow = false;
+}
+
+
+run();
+
+// {{POST_RUN_ADDITIONS}}
+
+
+
+
+
+
+// {{MODULE_ADDITIONS}}
+
+
+
+ var encoder_init = Module._encoder_init,
+ encoder_clear = Module._encoder_clear,
+ encoder_analysis_buffer = Module._encoder_analysis_buffer,
+ encoder_process = Module._encoder_process,
+ encoder_data_len = Module._encoder_data_len,
+ encoder_transfer_data = Module._encoder_transfer_data,
+ HEAPU8 = Module.HEAPU8,
+ HEAPU32 = Module.HEAPU32,
+ HEAPF32 = Module.HEAPF32;
+
+ var Encoder = function(sampleRate, numChannels, quality) {
+ this.numChannels = numChannels;
+ /**
+ * @type {Uint8Array[]}
+ */
+ this.oggBuffers = [];
+ this.encoder = encoder_init(this.numChannels, sampleRate, quality);
+ };
+
+ Encoder.prototype.encode = function(buffers) {
+ var length = buffers[0].length;
+ var analysis_buffer = encoder_analysis_buffer(this.encoder, length) >> 2;
+ for (var ch = 0; ch < this.numChannels; ++ch)
+ HEAPF32.set(buffers[ch], HEAPU32[analysis_buffer + ch] >> 2);
+ this.process(length);
+ };
+
+ Encoder.prototype.finish = function() {
+ this.process(0);
+ const buff = this.oggBuffers.slice();
+ this.cleanup();
+ return buff;
+ };
+
+ Encoder.prototype.cancel = Encoder.prototype.cleanup = function() {
+ encoder_clear(this.encoder);
+ delete this.encoder;
+ delete this.oggBuffers;
+ };
+
+ Encoder.prototype.process = function(length) {
+ encoder_process(this.encoder, length);
+ var len = encoder_data_len(this.encoder);
+ if (len > 0) {
+ var data = encoder_transfer_data(this.encoder);
+ this.oggBuffers.push(new Uint8Array(HEAPU8.subarray(data, data + len)));
+ }
+ };
+
+ libvorbis.OggVorbisEncoder = Encoder;
+ })
+};
+
+if (typeof window !== 'undefined' && window === self)
+{
+ libvorbis.init();
+}
+
+export { libvorbis }
+
diff --git a/src/spessasynth_lib/midi_parser/midi_editor.js b/src/spessasynth_lib/midi_parser/midi_editor.js
index 92cdf2c6..1196a01b 100644
--- a/src/spessasynth_lib/midi_parser/midi_editor.js
+++ b/src/spessasynth_lib/midi_parser/midi_editor.js
@@ -8,7 +8,7 @@ import { DEFAULT_PERCUSSION } from '../synthetizer/synthetizer.js'
* @param ticks {number}
* @returns {MidiMessage}
*/
-function getGsOn(ticks)
+export function getGsOn(ticks)
{
return new MidiMessage(
ticks,
@@ -324,8 +324,11 @@ export function modifyMIDI(
});
if(!addedGs)
{
- // gs is not on, add it on the first track at index 0
- midi.tracks[0].splice(0, 0, getGsOn(0));
+ // gs is not on, add it on the first track at index 0 (or 1 if track name is first)
+ let index = 0;
+ if(midi.tracks[0][0].messageStatusByte === messageTypes.trackName)
+ index++;
+ midi.tracks[0].splice(index, 0, getGsOn(0));
SpessaSynthInfo("%cGS on not detected. Adding it.", consoleColors.info);
addedGs = true;
}
diff --git a/src/spessasynth_lib/midi_parser/rmidi_writer.js b/src/spessasynth_lib/midi_parser/rmidi_writer.js
index ef0e9eb3..adab824a 100644
--- a/src/spessasynth_lib/midi_parser/rmidi_writer.js
+++ b/src/spessasynth_lib/midi_parser/rmidi_writer.js
@@ -4,6 +4,7 @@ import { RiffChunk, writeRIFFChunk } from '../soundfont/read/riff_chunk.js'
import { getStringBytes } from '../utils/byte_functions/string.js'
import { messageTypes, midiControllers, MidiMessage } from './midi_message.js'
import { DEFAULT_PERCUSSION } from '../synthetizer/synthetizer.js'
+import { getGsOn } from './midi_editor.js'
/**
*
@@ -14,9 +15,14 @@ import { DEFAULT_PERCUSSION } from '../synthetizer/synthetizer.js'
*/
export function writeRMIDI(soundfontBinary, mid, soundfont)
{
- // add 1 to bank. See wiki About-RMIDI.md
+ // add 1 to bank. See wiki About-RMIDI
// also fix presets that don't exists since midiplayer6 doesn't seem to default to 0 when nonextistent...
- let system = "gs";
+ let system = "gm";
+ /**
+ * The unwanted system messages such as gm/gm2 on
+ * @type {{tNum: number, e: MidiMessage}[]}
+ */
+ let unwantedSystems = [];
mid.tracks.forEach((t, trackNum) => {
let hasBankSelects = false;
/**
@@ -61,6 +67,27 @@ export function writeRMIDI(soundfontBinary, mid, soundfont)
{
system = "xg";
}
+ else
+ if(
+ e.messageData[0] === 0x41 // roland
+ && e.messageData[2] === 0x42 // GS
+ && e.messageData[6] === 0x7F // Mode set
+ )
+ {
+ system = "gs";
+ }
+ else
+ if(
+ e.messageData[0] === 0x7E // non realtime
+ && e.messageData[2] === 0x09 // gm system
+ )
+ {
+ system = "gm";
+ unwantedSystems.push({
+ tNum: trackNum,
+ e: e
+ });
+ }
return;
}
const sysexChannel = [9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 14, 15][e.messageData[5] & 0x0F];
@@ -76,14 +103,14 @@ export function writeRMIDI(soundfontBinary, mid, soundfont)
{
if(soundfont.presets.findIndex(p => p.program === e.messageData[0] && p.bank === 128) === -1)
{
- e.messageData[0] = soundfont.presets.find(p => p.bank === 128).program;
+ e.messageData[0] = soundfont.presets.find(p => p.bank === 128)?.program || 0;
}
}
else
{
if (soundfont.presets.findIndex(p => p.program === e.messageData[0] && p.bank !== 128) === -1)
{
- e.messageData[0] = soundfont.presets.find(p => p.bank !== 128).program;
+ e.messageData[0] = soundfont.presets.find(p => p.bank !== 128)?.program || 0;
}
}
// check if this preset exists for program and bank
@@ -144,7 +171,19 @@ export function writeRMIDI(soundfontBinary, mid, soundfont)
));
}
}
- })
+ });
+ // make sure to put xg if gm
+ if(system !== "gs" && system !== "xg")
+ {
+ for(const m of unwantedSystems)
+ {
+ mid.tracks[m.tNum].splice(mid.tracks[m.tNum].indexOf(m.e), 1);
+ }
+ let index = 0;
+ if(mid.tracks[0][0].messageStatusByte === messageTypes.trackName)
+ index++;
+ mid.tracks[0].splice(index, 0, getGsOn(0));
+ }
const newMid = new IndexedByteArray(writeMIDIFile(mid).buffer);
// infodata for MidiPlayer6
diff --git a/src/spessasynth_lib/sequencer/worklet_sequencer/play.js b/src/spessasynth_lib/sequencer/worklet_sequencer/play.js
index f92b7715..467b0114 100644
--- a/src/spessasynth_lib/sequencer/worklet_sequencer/play.js
+++ b/src/spessasynth_lib/sequencer/worklet_sequencer/play.js
@@ -127,7 +127,7 @@ export function _playTo(time, ticks = undefined)
let ccV = event.messageData[1];
if(this.midiData.embeddedSoundFont && controllerNumber === midiControllers.bankSelect)
{
- // special case if the RMID is embedded: subtract 1 from bank. See wiki About-RMIDI.md
+ // special case if the RMID is embedded: subtract 1 from bank. See wiki About-RMIDI
ccV--;
}
this.synth.controllerChange(channel, controllerNumber, ccV);
diff --git a/src/spessasynth_lib/sequencer/worklet_sequencer/process_event.js b/src/spessasynth_lib/sequencer/worklet_sequencer/process_event.js
index 5c7bd6ce..c752d620 100644
--- a/src/spessasynth_lib/sequencer/worklet_sequencer/process_event.js
+++ b/src/spessasynth_lib/sequencer/worklet_sequencer/process_event.js
@@ -67,7 +67,7 @@ export function _processEvent(event, trackIndex)
break;
case messageTypes.controllerChange:
- // special case if the RMID is embedded: subtract 1 from bank. See wiki About-RMIDI.md
+ // special case if the RMID is embedded: subtract 1 from bank. See wiki About-RMIDI
let v = event.messageData[1];
if(this.midiData.embeddedSoundFont && event.messageData[0] === midiControllers.bankSelect)
{
diff --git a/src/spessasynth_lib/soundfont/read/samples.js b/src/spessasynth_lib/soundfont/read/samples.js
index 1dad6bee..684e17c4 100644
--- a/src/spessasynth_lib/soundfont/read/samples.js
+++ b/src/spessasynth_lib/soundfont/read/samples.js
@@ -4,6 +4,7 @@ import { readBytesAsUintLittleEndian, signedInt8} from "../../utils/byte_functio
import { stbvorbis } from '../../externals/stbvorbis_sync.min.js'
import { SpessaSynthWarn } from '../../utils/loggin.js'
import { readBytesAsString } from '../../utils/byte_functions/string.js'
+import { encodeVorbis } from '../../utils/encode_vorbis.js'
/**
* samples.js
@@ -11,95 +12,10 @@ import { readBytesAsString } from '../../utils/byte_functions/string.js'
* loads sample data, handles async loading of sf3 compressed samples
*/
-/**
- * Reads the generatorTranslator from the shdr read
- * @param sampleHeadersChunk {RiffChunk}
- * @param smplChunkData {IndexedByteArray}
- * @returns {Sample[]}
- */
-export function readSamples(sampleHeadersChunk, smplChunkData)
-{
- /**
- * @type {Sample[]}
- */
- let samples = [];
- let index = 0;
- while(sampleHeadersChunk.chunkData.length > sampleHeadersChunk.chunkData.currentIndex)
- {
- const sample = readSample(index, sampleHeadersChunk.chunkData, smplChunkData);
- samples.push(sample);
- index++;
- }
- // remove EOS
- if (samples.length > 1)
- {
- samples.pop();
- }
- return samples;
-}
-
-/**
- * Reads it into a sample
- * @param index {number}
- * @param sampleHeaderData {IndexedByteArray}
- * @param smplArrayData {IndexedByteArray}
- * @returns {Sample}
- */
-function readSample(index, sampleHeaderData, smplArrayData) {
-
- // read the sample name
- let sampleName = readBytesAsString(sampleHeaderData, 20);
-
- // read the sample start index
- let sampleStartIndex = readBytesAsUintLittleEndian(sampleHeaderData, 4) * 2;
-
- // read the sample end index
- let sampleEndIndex = readBytesAsUintLittleEndian(sampleHeaderData, 4) * 2;
-
- // read the sample looping start index
- let sampleLoopStartIndex = readBytesAsUintLittleEndian(sampleHeaderData, 4) * 2;
-
- // read the sample looping end index
- let sampleLoopEndIndex = readBytesAsUintLittleEndian(sampleHeaderData, 4) * 2;
-
- // read the sample rate
- let sampleRate = readBytesAsUintLittleEndian(sampleHeaderData, 4);
-
- // read the original sample pitch
- let samplePitch = sampleHeaderData[sampleHeaderData.currentIndex++];
- if(samplePitch === 255)
- {
- // if it's 255, then default to 60
- samplePitch = 60;
- }
-
- // readt the sample pitch correction
- let samplePitchCorrection = signedInt8(sampleHeaderData[sampleHeaderData.currentIndex++]);
-
-
- // read the link to the other channel
- let sampleLink = readBytesAsUintLittleEndian(sampleHeaderData, 2);
- let sampleType = readBytesAsUintLittleEndian(sampleHeaderData, 2);
-
-
-
- return new Sample(sampleName,
- sampleStartIndex,
- sampleEndIndex,
- sampleLoopStartIndex,
- sampleLoopEndIndex,
- sampleRate,
- samplePitch,
- samplePitchCorrection,
- sampleLink,
- sampleType,
- smplArrayData,
- index);
-}
-
export class BasicSample
{
/**
+ * The basic representation of a soundfont sample
* @param sampleName {string}
* @param sampleRate {number}
* @param samplePitch {number}
@@ -166,6 +82,12 @@ export class BasicSample
* @type {boolean}
*/
this.isCompressed = (sampleType & 0x10) > 0;
+
+ /**
+ * The compressed sample data if it was compressed by spessasynth
+ * @type {Uint8Array}
+ */
+ this.compressedData = undefined;
}
/**
@@ -177,6 +99,43 @@ export class BasicSample
e.name = "NotImplementedError";
throw e;
}
+
+ /**
+ * @param quality {number}
+ */
+ compressSample(quality)
+ {
+ // no need to compress
+ if(this.isCompressed)
+ {
+ return;
+ }
+ // compress, always mono!
+ try {
+ this.compressedData = encodeVorbis([this.getAudioData()], 1, this.sampleRate, quality);
+ // flag as compressed
+ this.sampleType |= 0x10;
+ this.isCompressed = true;
+ }
+ catch (e)
+ {
+ SpessaSynthWarn(`Failed to compress ${this.sampleName}. Leaving as uncompressed!`);
+ this.isCompressed = false;
+ this.compressedData = undefined;
+ this.sampleType &= -17;
+ }
+
+ }
+
+ /**
+ * @returns {Float32Array}
+ */
+ getAudioData()
+ {
+ const e = new Error("Not implemented");
+ e.name = "NotImplementedError";
+ throw e;
+ }
}
export class Sample extends BasicSample
@@ -251,6 +210,10 @@ export class Sample extends BasicSample
const smplArr = this.sampleDataArray;
if(this.isCompressed)
{
+ if(this.compressedData)
+ {
+ return this.compressedData;
+ }
const smplStart = smplArr.currentIndex;
return smplArr.slice(this.sampleStartIndex / 2 + smplStart, this.sampleEndIndex / 2 + smplStart);
}
@@ -310,7 +273,7 @@ export class Sample extends BasicSample
}
// read the sample data
- let audioData = new Float32Array(this.sampleLength / 2 + 1);
+ let audioData = new Float32Array(this.sampleLength / 2);
const dataStartIndex = this.sampleDataArray.currentIndex;
let convertedSigned16 = new Int16Array(
this.sampleDataArray.slice(dataStartIndex + this.sampleStartIndex, dataStartIndex + this.sampleEndIndex)
@@ -347,4 +310,90 @@ export class Sample extends BasicSample
}
return this.loadUncompressedData();
}
+}
+
+/**
+ * Reads the generatorTranslator from the shdr read
+ * @param sampleHeadersChunk {RiffChunk}
+ * @param smplChunkData {IndexedByteArray}
+ * @returns {Sample[]}
+ */
+export function readSamples(sampleHeadersChunk, smplChunkData)
+{
+ /**
+ * @type {Sample[]}
+ */
+ let samples = [];
+ let index = 0;
+ while(sampleHeadersChunk.chunkData.length > sampleHeadersChunk.chunkData.currentIndex)
+ {
+ const sample = readSample(index, sampleHeadersChunk.chunkData, smplChunkData);
+ samples.push(sample);
+ index++;
+ }
+ // remove EOS
+ if (samples.length > 1)
+ {
+ samples.pop();
+ }
+ return samples;
+}
+
+/**
+ * Reads it into a sample
+ * @param index {number}
+ * @param sampleHeaderData {IndexedByteArray}
+ * @param smplArrayData {IndexedByteArray}
+ * @returns {Sample}
+ */
+function readSample(index, sampleHeaderData, smplArrayData) {
+
+ // read the sample name
+ let sampleName = readBytesAsString(sampleHeaderData, 20);
+
+ // read the sample start index
+ let sampleStartIndex = readBytesAsUintLittleEndian(sampleHeaderData, 4) * 2;
+
+ // read the sample end index
+ let sampleEndIndex = readBytesAsUintLittleEndian(sampleHeaderData, 4) * 2;
+
+ // read the sample looping start index
+ let sampleLoopStartIndex = readBytesAsUintLittleEndian(sampleHeaderData, 4) * 2;
+
+ // read the sample looping end index
+ let sampleLoopEndIndex = readBytesAsUintLittleEndian(sampleHeaderData, 4) * 2;
+
+ // read the sample rate
+ let sampleRate = readBytesAsUintLittleEndian(sampleHeaderData, 4);
+
+ // read the original sample pitch
+ let samplePitch = sampleHeaderData[sampleHeaderData.currentIndex++];
+ if(samplePitch === 255)
+ {
+ // if it's 255, then default to 60
+ samplePitch = 60;
+ }
+
+ // readt the sample pitch correction
+ let samplePitchCorrection = signedInt8(sampleHeaderData[sampleHeaderData.currentIndex++]);
+
+
+ // read the link to the other channel
+ let sampleLink = readBytesAsUintLittleEndian(sampleHeaderData, 2);
+ let sampleType = readBytesAsUintLittleEndian(sampleHeaderData, 2);
+
+
+
+ return new Sample(sampleName,
+ sampleStartIndex,
+ sampleEndIndex,
+ sampleLoopStartIndex,
+ sampleLoopEndIndex,
+ sampleRate,
+ samplePitch,
+ samplePitchCorrection,
+ sampleLink,
+ sampleType,
+ smplArrayData,
+ index);
}
\ No newline at end of file
diff --git a/src/spessasynth_lib/soundfont/write/sdta.js b/src/spessasynth_lib/soundfont/write/sdta.js
index 3309b103..9bd17446 100644
--- a/src/spessasynth_lib/soundfont/write/sdta.js
+++ b/src/spessasynth_lib/soundfont/write/sdta.js
@@ -7,13 +7,21 @@ import { consoleColors } from '../../utils/other.js'
* @this {SoundFont2}
* @param smplStartOffsets {number[]}
* @param smplEndOffsets {number[]}
+ * @param compress {boolean}
+ * @param quality {number}
* @returns {IndexedByteArray}
*/
-export function getSDTA(smplStartOffsets, smplEndOffsets)
+export function getSDTA(smplStartOffsets, smplEndOffsets, compress, quality)
{
// write smpl: write int16 data of each sample linearly
// get size (calling getAudioData twice doesn't matter since it gets cached)
- const sampleDatas = this.samples.map(s => s.getRawData());
+ const sampleDatas = this.samples.map(s => {
+ if(compress)
+ {
+ s.compressSample(quality);
+ }
+ return s.getRawData();
+ });
const smplSize = this.samples.reduce((total, s, i) => {
return total + sampleDatas[i].length + 46;
}, 0);
diff --git a/src/spessasynth_lib/soundfont/write/soundfont_trimmer.js b/src/spessasynth_lib/soundfont/write/soundfont_trimmer.js
index 96ba622e..2f356f7e 100644
--- a/src/spessasynth_lib/soundfont/write/soundfont_trimmer.js
+++ b/src/spessasynth_lib/soundfont/write/soundfont_trimmer.js
@@ -13,7 +13,7 @@ import { messageTypes, midiControllers } from '../../midi_parser/midi_message.js
* @param mid {MIDI}
* @returns {Uint8Array}
*/
-export function getTrimmedSoundfont(soundfont, mid)
+export function trimSoundfont(soundfont, mid)
{
/**
* @param instrument {Instrument}
@@ -317,5 +317,4 @@ export function getTrimmedSoundfont(soundfont, mid)
consoleColors.recognized)
SpessaSynthGroupEnd();
SpessaSynthGroupEnd();
- return soundfont.write();
}
\ No newline at end of file
diff --git a/src/spessasynth_lib/soundfont/write/write.js b/src/spessasynth_lib/soundfont/write/write.js
index e7f0d61d..1255ab52 100644
--- a/src/spessasynth_lib/soundfont/write/write.js
+++ b/src/spessasynth_lib/soundfont/write/write.js
@@ -19,15 +19,35 @@ import {
SpessaSynthInfo,
} from '../../utils/loggin.js'
+/**
+ * @typedef {Object} SoundFont2WriteOptions
+ * @property {boolean} compress - if the soundfont should be compressed with the ogg vorbis codec
+ * @property {number} compressionQuality - the vorbis compression quality, from -0.1 to 1
+ */
+
+/**
+ * @type {SoundFont2WriteOptions}
+ */
+const DEFAULT_WRITE_OPTIONS = {
+ compress: false,
+ compressionQuality: 0.5
+}
+
/**
* Write the soundfont as an .sf2 file. This method is DESTRUCTIVE
* @this {SoundFont2}
+ * @param {SoundFont2WriteOptions} options
* @returns {Uint8Array}
*/
-export function write()
+export function write(options = DEFAULT_WRITE_OPTIONS)
{
SpessaSynthGroupCollapsed("%cSaving soundfont...",
consoleColors.info);
+ SpessaSynthInfo(`%cCompression: %c${options?.compress || "false"}%c quality: %c${options?.compressionQuality || "none"}`,
+ consoleColors.info,
+ consoleColors.recognized,
+ consoleColors.info,
+ consoleColors.recognized)
SpessaSynthInfo("%cWriting INFO...",
consoleColors.info);
/**
@@ -36,13 +56,16 @@ export function write()
*/
const infoArrays = [];
this.soundFontInfo["ISFT"] = "SpessaSynth"; // ( ͡° ͜ʖ ͡°)
- this.soundFontInfo["ifil"] = this.soundFontInfo["ifil"].split(".")[0] + ".4"; // always vesrion 4
+ if(options?.compress)
+ {
+ this.soundFontInfo["ifil"] = "3.0"; // set version to 3
+ }
for (const [type, data] of Object.entries(this.soundFontInfo))
{
if(type === "ifil" || type === "iver")
{
const major= parseInt(data.split(".")[0]);
- const minor = parseInt(data.split(".")[1]);
+ const minor= parseInt(data.split(".")[1]);
const ckdata = new IndexedByteArray(4);
writeWord(ckdata, major);
writeWord(ckdata, minor);
@@ -74,7 +97,7 @@ export function write()
// write sdata
const smplStartOffsets = [];
const smplEndOffsets = [];
- const sdtaChunk = getSDTA.bind(this)(smplStartOffsets, smplEndOffsets);
+ const sdtaChunk = getSDTA.bind(this)(smplStartOffsets, smplEndOffsets, options?.compress, options?.compressionQuality || 0.5);
SpessaSynthInfo("%cWriting PDTA...",
consoleColors.info);
diff --git a/src/spessasynth_lib/utils/encode_vorbis.js b/src/spessasynth_lib/utils/encode_vorbis.js
new file mode 100644
index 00000000..8b5d219d
--- /dev/null
+++ b/src/spessasynth_lib/utils/encode_vorbis.js
@@ -0,0 +1,29 @@
+import { libvorbis } from '../externals/OggVorbisEncoder.js'
+
+/**
+ * @param channelAudioData {Float32Array[]}
+ * @param sampleRate {number}
+ * @param channels {number}
+ * @param quality {number} -0.1 to 1
+ * @returns {Uint8Array}
+ */
+export function encodeVorbis(channelAudioData, channels, sampleRate, quality)
+{
+ // https://github.com/higuma/ogg-vorbis-encoder-js
+ //libvorbis.init();
+ const encoder = new libvorbis.OggVorbisEncoder(sampleRate, channels, quality);
+ encoder.encode(channelAudioData);
+ /**
+ * @type {Uint8Array[]}
+ */
+ const arrs = encoder.finish();
+ const outLen = arrs.reduce((l, c) => l + c.length, 0);
+ const out = new Uint8Array(outLen);
+ let offset = 0;
+ for(const a of arrs)
+ {
+ out.set(a, offset);
+ offset += a.length;
+ }
+ return out;
+}
\ No newline at end of file
diff --git a/src/website/css/notification/buttons.css b/src/website/css/notification/buttons.css
index 6e60b6de..25524b6a 100644
--- a/src/website/css/notification/buttons.css
+++ b/src/website/css/notification/buttons.css
@@ -5,7 +5,6 @@
margin: 0.5rem;
cursor: pointer;
border-radius: var(--notification-border-radius);
- font-size: 1em;
- width: 70%;
+ font-size: var(--notification-font-size);
min-width: fit-content;
}
\ No newline at end of file
diff --git a/src/website/css/notification/inputs.css b/src/website/css/notification/inputs.css
index c1e5bea6..09111705 100644
--- a/src/website/css/notification/inputs.css
+++ b/src/website/css/notification/inputs.css
@@ -6,13 +6,13 @@
margin: 0.5rem;
}
-input:not([type='checkbox'])::-webkit-outer-spin-button,
-input:not([type='checkbox'])::-webkit-inner-spin-button {
+input[type='number']::-webkit-outer-spin-button,
+input[type='number']::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
-.notification input:not([type='checkbox']){
+.notification input[type='number']{
display: block;
background: var(--top-buttons-color);
border: solid 1px #333;
@@ -21,4 +21,22 @@ input:not([type='checkbox'])::-webkit-inner-spin-button {
border-radius: var(--notification-border-radius);
width: 5rem;
-moz-appearance: textfield;
-}
\ No newline at end of file
+}
+
+.notification_slider_wrapper{
+ display: flex;
+ justify-content: space-between;
+ margin: 0.5rem;
+}
+
+.notification_slider_wrapper label{
+ margin-right: 2rem;
+}
+
+.notification_slider_wrapper .settings_visual_wrapper{
+ margin: 0 !important;
+}
+
+.notification_slider_wrapper .settings_slider_wrapper{
+ width: auto !important;
+}
diff --git a/src/website/css/notification/notification.css b/src/website/css/notification/notification.css
index 936abe98..2d315fe4 100644
--- a/src/website/css/notification/notification.css
+++ b/src/website/css/notification/notification.css
@@ -6,6 +6,8 @@
.notification {
--notification-border-radius: var(--primary-border-radius);
+ --notification-font-size: 1.3rem;
+
background: var(--top-color);
color: var(--font-color);
position: fixed;
@@ -44,14 +46,13 @@
.notification h2{
text-align: start;
- font-size: 1.5rem;
line-height: 2rem;
margin: 1rem;
}
.notification_content{
margin: 1rem;
- width: 90%;
+ min-width: 90%;
}
.notification .close_btn{
diff --git a/src/website/css/notification/texts.css b/src/website/css/notification/texts.css
index 1f07aca7..32655048 100644
--- a/src/website/css/notification/texts.css
+++ b/src/website/css/notification/texts.css
@@ -1,3 +1,8 @@
.notification p{
margin: 1rem;
+ font-size: var(--notification-font-size);
+}
+
+.notification label{
+ font-size: var(--notification-font-size);
}
\ No newline at end of file
diff --git a/src/website/js/notification/get_content.js b/src/website/js/notification/get_content.js
index e7263147..ae936abf 100644
--- a/src/website/js/notification/get_content.js
+++ b/src/website/js/notification/get_content.js
@@ -1,3 +1,5 @@
+import { createSlider } from '../settings_ui/sliders.js'
+
/**
* @param el {HTMLElement}
* @param content {NotificationContent}
@@ -66,6 +68,20 @@ export function getContent(content, locale)
case "toggle":
return getSwitch(content, locale);
+
+ case "range":
+ const range = document.createElement("input");
+ range.type = "range";
+ const label = document.createElement("label");
+ applyAttributes(content, [range, label]);
+ applyTextContent(label, content, locale);
+ const slider = createSlider(range, false);
+ const wrapper = document.createElement("div");
+ wrapper.classList.add("notification_slider_wrapper");
+ wrapper.appendChild(label);
+ wrapper.appendChild(slider);
+ return wrapper;
+
}
}
diff --git a/src/website/js/notification/notification.js b/src/website/js/notification/notification.js
index bfef253d..63cad41f 100644
--- a/src/website/js/notification/notification.js
+++ b/src/website/js/notification/notification.js
@@ -17,7 +17,7 @@ const notifications = {};
/**
* @typedef {Object} NotificationContent
- * @property {"button"|"progress"|"text"|"input"|"toggle"} type
+ * @property {"button"|"progress"|"text"|"input"|"toggle"|"range"} type
* @property {string|undefined} textContent
* @property {string|undefined} translatePathTitle
* @property {Object|undefined} attributes
@@ -30,6 +30,7 @@ const notifications = {};
* @param time {number} seconds
* @param allowClosing {boolean}
* @param locale {LocaleManager}
+ * @param contentStyling {Object}
* @returns {NotificationType}
*/
export function showNotification(
@@ -37,7 +38,8 @@ export function showNotification(
contents,
time = NOTIFICATION_TIME,
allowClosing = true,
- locale = undefined)
+ locale = undefined,
+ contentStyling = undefined)
{
const notification = document.createElement("div");
const notificationID = notificationCounter++;
@@ -50,6 +52,13 @@ export function showNotification(
`;
const contentWrapper = document.createElement("div");
contentWrapper.classList.add("notification_content");
+ if(contentStyling)
+ {
+ for(const [key, value] of Object.entries(contentStyling))
+ {
+ contentWrapper.style[key] = value;
+ }
+ }
notification.appendChild(contentWrapper);
for(const content of contents)
{
diff --git a/src/website/js/settings_ui/sliders.js b/src/website/js/settings_ui/sliders.js
index a89099cf..afde32ca 100644
--- a/src/website/js/settings_ui/sliders.js
+++ b/src/website/js/settings_ui/sliders.js
@@ -18,63 +18,74 @@ export function handleSliders(div)
const inputs = div.getElementsByTagName("spessarange");
for(const input of inputs)
{
- // main wrapper wraps the visual wrapper and span
- const mainWrapper = document.createElement("div");
- mainWrapper.classList.add("settings_slider_wrapper");
- // copy over values to the actual input
- const min = input.getAttribute("min");
- const max = input.getAttribute("max");
- const current = input.getAttribute("value");
- const units = input.getAttribute("units");
- const id = input.getAttribute("input_id");
- const htmlInput = document.createElement("input");
- htmlInput.classList.add("settings_slider");
- htmlInput.type = "range";
- htmlInput.id = id;
- htmlInput.min = min;
- htmlInput.max = max;
- htmlInput.value = current;
+ input.parentElement.insertBefore(createSlider(input, true), input);
+ }
+ while(inputs.length > 0)
+ {
+ inputs[0].parentNode.removeChild(inputs[0]);
+ }
+}
+
+export function createSlider(input, showSpan = true)
+{
+ // main wrapper wraps the visual wrapper and span
+ const mainWrapper = document.createElement("div");
+ mainWrapper.classList.add("settings_slider_wrapper");
+ // copy over values to the actual input
+ const min = input.getAttribute( "min");
+ const max = input.getAttribute("max");
+ const current = input.getAttribute("value");
+ const units = input.getAttribute("units");
+ const id = input.getAttribute("input_id");
+ const htmlInput = document.createElement("input");
+ htmlInput.classList.add("settings_slider");
+ htmlInput.type = "range";
+ htmlInput.id = id;
+ htmlInput.min = min;
+ htmlInput.max = max;
+ htmlInput.value = current;
- const span = document.createElement("span");
+ let span
+ if(showSpan)
+ {
+ span = document.createElement("span");
span.textContent = current + units;
+ }
- // visual wrapper wraps the input, thumb and progress
- const visualWrapper = document.createElement("div");
- visualWrapper.classList.add("settings_visual_wrapper");
+ // visual wrapper wraps the input, thumb and progress
+ const visualWrapper = document.createElement("div");
+ visualWrapper.classList.add("settings_visual_wrapper");
- const progressBar = document.createElement("div");
- progressBar.classList.add("settings_slider_progress");
- visualWrapper.appendChild(progressBar);
+ const progressBar = document.createElement("div");
+ progressBar.classList.add("settings_slider_progress");
+ visualWrapper.appendChild(progressBar);
- const thumb = document.createElement("div");
- thumb.classList.add("settings_slider_thumb");
- visualWrapper.appendChild(thumb);
- visualWrapper.appendChild(htmlInput);
+ const thumb = document.createElement("div");
+ thumb.classList.add("settings_slider_thumb");
+ visualWrapper.appendChild(thumb);
+ visualWrapper.appendChild(htmlInput);
- htmlInput.addEventListener("input", () => {
- // calculate the difference between values, if larger than 5%, enable transition
- const val = parseInt(visualWrapper.style.getPropertyValue("--visual-width").replace("%", ""));
- const newVal = Math.round((htmlInput.value - htmlInput.min) / (htmlInput.max - htmlInput.min) * 100);
- if(Math.abs((val - newVal) / 100) > 0.05)
- {
- visualWrapper.classList.add("settings_slider_transition");
- }
- else
- {
- visualWrapper.classList.remove("settings_slider_transition");
- }
- // apply the width
- visualWrapper.style.setProperty("--visual-width", `${newVal}%`);
- });
- visualWrapper.style.setProperty("--visual-width", `${(htmlInput.value - htmlInput.min) / (htmlInput.max - htmlInput.min) * 100}%`);
- const parent = input.parentElement;
- mainWrapper.appendChild(visualWrapper);
- mainWrapper.appendChild(span);
- parent.insertBefore(mainWrapper, input);
- }
- while(inputs.length > 0)
+ htmlInput.addEventListener("input", () => {
+ // calculate the difference between values, if larger than 5%, enable transition
+ const val = parseInt(visualWrapper.style.getPropertyValue("--visual-width").replace("%", ""));
+ const newVal = Math.round((htmlInput.value - htmlInput.min) / (htmlInput.max - htmlInput.min) * 100);
+ if(Math.abs((val - newVal) / 100) > 0.05)
+ {
+ visualWrapper.classList.add("settings_slider_transition");
+ }
+ else
+ {
+ visualWrapper.classList.remove("settings_slider_transition");
+ }
+ // apply the width
+ visualWrapper.style.setProperty("--visual-width", `${newVal}%`);
+ });
+ visualWrapper.style.setProperty("--visual-width", `${(htmlInput.value - htmlInput.min) / (htmlInput.max - htmlInput.min) * 100}%`);
+ mainWrapper.appendChild(visualWrapper);
+ if(showSpan)
{
- inputs[0].parentNode.removeChild(inputs[0]);
+ mainWrapper.appendChild(span);
}
+ return mainWrapper;
}
\ No newline at end of file
diff --git a/src/website/js/soundfont_mixer.js b/src/website/js/soundfont_mixer.js
index 6fd83262..b7a50fd7 100644
--- a/src/website/js/soundfont_mixer.js
+++ b/src/website/js/soundfont_mixer.js
@@ -76,7 +76,7 @@ export class SoundFontMixer
{
// compile the soundfont from first to last
let soundfont = SoundFont2.mergeSoundfonts(...this.soundfontList.map(s => s.soundFont));
- this.synth.reloadSoundFont(soundfont.write()).then();
+ this.synth.reloadSoundFont(soundfont.write().buffer).then();
}
/**
diff --git a/src/website/locale/locale_files/locale_en/export_audio.js b/src/website/locale/locale_files/locale_en/export_audio.js
index 630aed46..0d420e21 100644
--- a/src/website/locale/locale_files/locale_en/export_audio.js
+++ b/src/website/locale/locale_files/locale_en/export_audio.js
@@ -13,7 +13,7 @@ export const exportAudio = {
description: "Export the song with modifications as a .wav audio file"
},
options: {
- title: "Audio export options",
+ title: "WAV export options",
confirm: "Export",
normalizeVolume: {
title: "Normalize volume",
@@ -25,7 +25,7 @@ export const exportAudio = {
}
},
exportMessage: {
- message: "Exporting audio...",
+ message: "Exporting WAV audio...",
estimated: "Remaining:"
}
},
@@ -41,6 +41,20 @@ export const exportAudio = {
button: {
title: "Trimmed soundfont",
description: "Export the soundfont trimmed to only use instruments and samples that the MIDI file uses"
+ },
+
+ options: {
+ title: "SF export options",
+ confirm: "Export",
+ compress: {
+ title: "Compress",
+ description: "Compress samples with lossy Ogg Vorbis compression if uncompressed. Significantly reduces the file size." +
+ "If the soundfont was already compressed, it won't be uncompressed even if this option is disabled"
+ },
+ quality: {
+ title: "Compression quality",
+ description: "The quality of compression. Higher is better"
+ }
}
},
@@ -58,6 +72,19 @@ export const exportAudio = {
modifyingSoundfont: "Trimming Soundfont...",
saving: "Saving RMIDI...",
done: "Done!"
+ },
+
+ options: {
+ title: "RMIDI export options",
+ confirm: "Export",
+ compress: {
+ title: "Compress",
+ description: "Compress the Soundfont with lossy Ogg Vorbis compression. Significantly reduces the file size. Recommended."
+ },
+ quality: {
+ title: "Compression quality",
+ description: "The quality of compression. Higher is better."
+ }
}
}
}
diff --git a/src/website/locale/locale_files/locale_ja/export_audio.js b/src/website/locale/locale_files/locale_ja/export_audio.js
index 22703a4f..f78efe3d 100644
--- a/src/website/locale/locale_files/locale_ja/export_audio.js
+++ b/src/website/locale/locale_files/locale_ja/export_audio.js
@@ -41,6 +41,19 @@ export const exportAudio = {
button: {
title: "トリミングされたサウンドフォント",
description: "MIDIファイルで使用されている楽器とサンプルだけにトリミングされたサウンドフォントをエクスポートします"
+ },
+ options: {
+ title: "SFエクスポートオプション",
+ confirm: "エクスポート",
+ compress: {
+ title: "圧縮",
+ description: "未圧縮のサンプルをOgg Vorbisのロス圧縮で圧縮します。ファイルサイズが大幅に削減されます。" +
+ "サウンドフォントがすでに圧縮されている場合は、このオプションを無効にしても再圧縮されることはありません"
+ },
+ quality: {
+ title: "圧縮品質",
+ description: "圧縮の品質です。高いほど良い"
+ }
}
},
@@ -50,7 +63,6 @@ export const exportAudio = {
description: "変更されたMIDIとトリミングされたサウンドフォントを1つのファイルに埋め込んでエクスポートします。 " +
"この形式は広くサポートされていないことに注意してください"
},
-
progress: {
title: "埋め込まれたMIDIをエクスポート中...",
loading: "サウンドフォントとMIDIを読み込み中...",
@@ -58,6 +70,18 @@ export const exportAudio = {
modifyingSoundfont: "サウンドフォントをトリミング中...",
saving: "RMIDIを保存中...",
done: "完了しました!"
+ },
+ options: {
+ title: "RMIDIエクスポートオプション",
+ confirm: "エクスポート",
+ compress: {
+ title: "圧縮",
+ description: "サウンドフォントをOgg Vorbisのロス圧縮で圧縮します。ファイルサイズが大幅に削減されます。推奨設定です。"
+ },
+ quality: {
+ title: "圧縮品質",
+ description: "圧縮の品質です。高いほど良い"
+ }
}
}
}
diff --git a/src/website/locale/locale_files/locale_pl/export_audio.js b/src/website/locale/locale_files/locale_pl/export_audio.js
index e580429a..508e6458 100644
--- a/src/website/locale/locale_files/locale_pl/export_audio.js
+++ b/src/website/locale/locale_files/locale_pl/export_audio.js
@@ -41,6 +41,20 @@ export const exportAudio = {
button: {
title: "Zmniejszony soundfont",
description: "Eksportuj soundfont zawierający tylko klawisze użyte w MIDI"
+ },
+
+ options: {
+ title: "Opcje eksportu soundfonta",
+ confirm: "Eksportuj",
+ compress: {
+ title: "Kompresuj",
+ description: "Zkompresuj próbki które nie są zkompresowane przy użyciu stratnego kodeka Ogg Vorbis. Znacznie zmniejsza rozmiar pliku." +
+ "Jeśli soundfont był już skompresowany, nie zostanie zdekompresowany nawet gdy ta opcja jest wyłączona"
+ },
+ quality: {
+ title: "Jakość kompresji",
+ description: "Jakość skompresowanych próbek. Im wyższa tym lepsza"
+ }
}
},
@@ -58,6 +72,19 @@ export const exportAudio = {
modifyingSoundfont: "Zmniejszanie Soundfonta...",
saving: "Zapisywanie RMIDI...",
done: "Gotowe!"
+ },
+
+ options: {
+ title: "Opcje eksportu RMIDI",
+ confirm: "Eksportuj",
+ compress: {
+ title: "Kompresuj",
+ description: "Skompresuj osadzonego soundfonta za pomocą stratnego kodeka Ogg Vorbis. Znacznie zmniejsza rozmiar pliku. Zalecane."
+ },
+ quality: {
+ title: "Jakość kompresji",
+ description: "Jakość skompresowanych próbek. Im wyższa tym lepsza"
+ }
}
}
}
diff --git a/src/website/manager/export_wav.js b/src/website/manager/export_audio.js
similarity index 71%
rename from src/website/manager/export_wav.js
rename to src/website/manager/export_audio.js
index b6a33800..06b6aebb 100644
--- a/src/website/manager/export_wav.js
+++ b/src/website/manager/export_audio.js
@@ -12,7 +12,7 @@ const RENDER_AUDIO_TIME_INTERVAL = 1000;
* @returns {Promise}
* @private
*/
-export async function _doExporWav(normalizeAudio = true, additionalTime = 2)
+export async function _doExportAudioData(normalizeAudio = true, additionalTime = 2)
{
this.isExporting = true;
if(!this.seq)
@@ -20,8 +20,8 @@ export async function _doExporWav(normalizeAudio = true, additionalTime = 2)
throw new Error("No sequencer active");
}
// get locales
- const exportingMessage = manager.localeManager.getLocaleString("locale.exportAudio.formats.formats.wav.exportMessage.message");
- const estimatedMessage = manager.localeManager.getLocaleString("locale.exportAudio.formats.formats.wav.exportMessage.estimated");
+ const exportingMessage = manager.localeManager.getLocaleString(`locale.exportAudio.formats.formats.wav.exportMessage.message`);
+ const estimatedMessage = manager.localeManager.getLocaleString(`locale.exportAudio.formats.formats.wav.exportMessage.estimated`);
const notification = showNotification(
exportingMessage,
[
@@ -108,7 +108,7 @@ export async function _doExporWav(normalizeAudio = true, additionalTime = 2)
// clear intervals and save file
clearInterval(interval);
closeNotification(notification.id);
- this.saveBlob(audioBufferToWav(buf, normalizeAudio), `${window.manager.seq.midiData.midiName || 'unnamed_song'}.wav`)
+ this.saveBlob(audioBufferToWav(buf, normalizeAudio), `${this.seq.midiData.midiName || 'unnamed_song'}.wav`);
this.isExporting = false;
}
@@ -117,43 +117,51 @@ export async function _doExporWav(normalizeAudio = true, additionalTime = 2)
* @returns {Promise}
* @private
*/
-export async function _exportWav()
+export async function _exportAudioData()
{
if(this.isExporting)
{
return;
}
- const path = "locale.exportAudio.formats.formats.wav.options.";
- showNotification(
- this.localeManager.getLocaleString(path + "title"),
- [
- {
- type: "toggle",
- translatePathTitle: path + "normalizeVolume",
- attributes: {
- "normalize-volume-toggle": "1",
- "checked": "true"
- }
- },
- {
- type: "input",
- translatePathTitle: path + "additionalTime",
- attributes: {
- "value": "2",
- "type": "number"
- }
- },
- {
- type: "button",
- textContent: this.localeManager.getLocaleString(path + "confirm"),
- onClick: n => {
- closeNotification(n.id);
- const normalizeVolume = n.div.querySelector("input[normalize-volume-toggle='1']").checked;
- const additionalTime = n.div.querySelector("input[type='number']").value;
- this._doExportWav(normalizeVolume, parseInt(additionalTime));
- }
+ const wavPath = `locale.exportAudio.formats.formats.wav.options.`;
+ /**
+ * @type {NotificationContent[]}
+ */
+ const WAV_OPTIONS = [
+ {
+ type: "toggle",
+ translatePathTitle: wavPath + "normalizeVolume",
+ attributes: {
+ "normalize-volume-toggle": "1",
+ "checked": "true"
}
- ],
+ },
+ {
+ type: "input",
+ translatePathTitle: wavPath + "additionalTime",
+ attributes: {
+ "value": "2",
+ "type": "number"
+ }
+ },
+ {
+ type: "button",
+ textContent: this.localeManager.getLocaleString(wavPath + "confirm"),
+ onClick: n => {
+ closeNotification(n.id);
+ const normalizeVolume = n.div.querySelector("input[normalize-volume-toggle='1']").checked;
+ const additionalTime = n.div.querySelector("input[type='number']").value;
+ this._doExportAudioData(normalizeVolume, parseInt(additionalTime));
+ }
+ }
+ ];
+
+ /**
+ * @type {NotificationContent[]}
+ */
+ showNotification(
+ this.localeManager.getLocaleString(wavPath + "title"),
+ WAV_OPTIONS,
9999999,
true,
this.localeManager
diff --git a/src/website/manager/export_rmidi.js b/src/website/manager/export_rmidi.js
index 8004e6c6..6a011052 100644
--- a/src/website/manager/export_rmidi.js
+++ b/src/website/manager/export_rmidi.js
@@ -1,8 +1,8 @@
import { SoundFont2 } from '../../spessasynth_lib/soundfont/soundfont.js'
-import { getTrimmedSoundfont } from '../../spessasynth_lib/soundfont/write/soundfont_trimmer.js'
+import { trimSoundfont } from '../../spessasynth_lib/soundfont/write/soundfont_trimmer.js'
import { applySnapshotToMIDI } from '../../spessasynth_lib/midi_parser/midi_editor.js'
import { closeNotification, showNotification } from '../js/notification/notification.js'
-import { SpessaSynthGroupCollapsed, SpessaSynthGroupEnd } from '../../spessasynth_lib/utils/loggin.js'
+import { SpessaSynthGroup, SpessaSynthGroupCollapsed, SpessaSynthGroupEnd } from '../../spessasynth_lib/utils/loggin.js'
import { consoleColors } from '../../spessasynth_lib/utils/other.js'
import { writeRMIDI } from '../../spessasynth_lib/midi_parser/rmidi_writer.js'
@@ -13,44 +13,81 @@ import { writeRMIDI } from '../../spessasynth_lib/midi_parser/rmidi_writer.js'
*/
export async function _exportRMIDI()
{
- SpessaSynthGroupCollapsed("%cExporting RMIDI...",
- consoleColors.info);
- const localePath = "locale.exportAudio.formats.formats.rmidi.progress.";
- const notification = showNotification(
- this.localeManager.getLocaleString(localePath + "title"),
- [{
- type: "text",
- textContent: this.localeManager.getLocaleString(localePath + "loading"),
- attributes: {
- "class": "export_rmidi_message"
- }
- }],
- 9999999,
- false
- );
- // allow the notification to show
- await new Promise(r => setTimeout(r, 500));
- const message = notification.div.getElementsByClassName("export_rmidi_message")[0];
- const mid = await this.seq.getMIDI();
- const font = new SoundFont2(mid.embeddedSoundFont || this.soundFont);
+ const path = "locale.exportAudio.formats.formats.rmidi.options.";
+ showNotification(
+ this.localeManager.getLocaleString(path + "title"),
+ [
+ {
+ type: "toggle",
+ translatePathTitle: path + "compress",
+ attributes: {
+ "compress-toggle": "1",
+ "checked": "true"
+ }
+ },
+ {
+ type: "range",
+ translatePathTitle: path + "quality",
+ attributes: {
+ "min": "0",
+ "max": "10",
+ "value": "5"
+ }
+ },
+ {
+ type: "button",
+ textContent: this.localeManager.getLocaleString(path + "confirm"),
+ onClick: async n => {
+ const compressed = n.div.querySelector("input[compress-toggle='1']").checked;
+ const quality = parseInt(n.div.querySelector("input[type='range']").value) / 10;
+ closeNotification(n.id);
+
+ SpessaSynthGroupCollapsed("%cExporting RMIDI...",
+ consoleColors.info);
+ const localePath = "locale.exportAudio.formats.formats.rmidi.progress.";
+ const notification = showNotification(
+ this.localeManager.getLocaleString(localePath + "title"),
+ [{
+ type: "text",
+ textContent: this.localeManager.getLocaleString(localePath + "loading"),
+ attributes: {
+ "class": "export_rmidi_message"
+ }
+ }],
+ 9999999,
+ false
+ );
+ // allow the notification to show
+ await new Promise(r => setTimeout(r, 500));
+ const message = notification.div.getElementsByClassName("export_rmidi_message")[0];
+ const mid = await this.seq.getMIDI();
+ const font = new SoundFont2(mid.embeddedSoundFont || this.soundFont);
- message.textContent = this.localeManager.getLocaleString(localePath + "modifyingMIDI");
- await new Promise(r => setTimeout(r, 10));
+ message.textContent = this.localeManager.getLocaleString(localePath + "modifyingMIDI");
+ await new Promise(r => setTimeout(r, 10));
- applySnapshotToMIDI(mid, await this.synth.getSynthesizerSnapshot());
+ applySnapshotToMIDI(mid, await this.synth.getSynthesizerSnapshot());
- message.textContent = this.localeManager.getLocaleString(localePath + "modifyingSoundfont");
- await new Promise(r => setTimeout(r, 10));
+ message.textContent = this.localeManager.getLocaleString(localePath + "modifyingSoundfont");
+ await new Promise(r => setTimeout(r, 10));
- const newFont = getTrimmedSoundfont(font, mid);
+ trimSoundfont(font, mid);
+ const newFont = font.write({compress: compressed, compressionQuality: quality});
- message.textContent = this.localeManager.getLocaleString(localePath + "saving");
- await new Promise(r => setTimeout(r, 10));
+ message.textContent = this.localeManager.getLocaleString(localePath + "saving");
+ await new Promise(r => setTimeout(r, 10));
- const rmidBinary = writeRMIDI(newFont, mid, font);
- const blob = new Blob([rmidBinary.buffer], {type: "audio/rmid"})
- this.saveBlob(blob, `${mid.midiName || "unnamed_song"}.rmi`);
- message.textContent = this.localeManager.getLocaleString(localePath + "done");
- closeNotification(notification.id);
- SpessaSynthGroupEnd();
+ const rmidBinary = writeRMIDI(newFont, mid, font);
+ const blob = new Blob([rmidBinary.buffer], {type: "audio/rmid"})
+ this.saveBlob(blob, `${mid.midiName || "unnamed_song"}.rmi`);
+ message.textContent = this.localeManager.getLocaleString(localePath + "done");
+ closeNotification(notification.id);
+ SpessaSynthGroupEnd();
+ }
+ }
+ ],
+ 9999999,
+ true,
+ this.localeManager
+ )
}
\ No newline at end of file
diff --git a/src/website/manager/export_song.js b/src/website/manager/export_song.js
index 4408570f..b6233b60 100644
--- a/src/website/manager/export_song.js
+++ b/src/website/manager/export_song.js
@@ -15,7 +15,7 @@ export async function exportSong()
translatePathTitle: path + "formats.wav.button",
onClick: n => {
closeNotification(n.id);
- this._exportWav();
+ this._exportAudioData();
}
},
{
@@ -71,6 +71,11 @@ export async function exportSong()
],
999999,
true,
- this.localeManager
+ this.localeManager,
+ {
+ display: "flex",
+ flexWrap: "wrap",
+ justifyContent: "center"
+ }
)
}
\ No newline at end of file
diff --git a/src/website/manager/export_soundfont.js b/src/website/manager/export_soundfont.js
index e4a311d0..5936bd0f 100644
--- a/src/website/manager/export_soundfont.js
+++ b/src/website/manager/export_soundfont.js
@@ -5,7 +5,8 @@ import {
SpessaSynthGroupEnd,
} from '../../spessasynth_lib/utils/loggin.js'
import { consoleColors } from '../../spessasynth_lib/utils/other.js'
-import { getTrimmedSoundfont } from '../../spessasynth_lib/soundfont/write/soundfont_trimmer.js'
+import { trimSoundfont } from '../../spessasynth_lib/soundfont/write/soundfont_trimmer.js'
+import { closeNotification, showNotification } from '../js/notification/notification.js'
/**
* @this {Manager}
@@ -14,14 +15,49 @@ import { getTrimmedSoundfont } from '../../spessasynth_lib/soundfont/write/sound
*/
export async function _exportSoundfont()
{
- SpessaSynthGroup("%cExporting minified soundfont...",
- consoleColors.info);
- const mid = await this.seq.getMIDI();
- const soundfont = new SoundFont2(mid.embeddedSoundFont || this.soundFont);
- applySnapshotToMIDI(mid, await this.synth.getSynthesizerSnapshot());
- const binary = getTrimmedSoundfont(soundfont, mid);
- const blob = new Blob([binary.buffer], {type: "audio/soundfont"});
- let extension = soundfont.soundFontInfo["ifil"].split(".")[0] === "3" ? "sf3" : "sf2";
- this.saveBlob(blob, `${soundfont.soundFontInfo['INAM'] || "unnamed"}.${extension}`);
- SpessaSynthGroupEnd();
+ const path = "locale.exportAudio.formats.formats.soundfont.options.";
+ showNotification(
+ this.localeManager.getLocaleString(path + "title"),
+ [
+ {
+ type: "toggle",
+ translatePathTitle: path + "compress",
+ attributes: {
+ "compress-toggle": "1",
+ }
+ },
+ {
+ type: "range",
+ translatePathTitle: path + "quality",
+ attributes: {
+ "min": "0",
+ "max": "10",
+ "value": "5"
+ }
+ },
+ {
+ type: "button",
+ textContent: this.localeManager.getLocaleString(path + "confirm"),
+ onClick: async n => {
+ const compressed = n.div.querySelector("input[compress-toggle='1']").checked;
+ const quality = parseInt(n.div.querySelector("input[type='range']").value) / 10;
+ closeNotification(n.id);
+ SpessaSynthGroup("%cExporting minified soundfont...",
+ consoleColors.info);
+ const mid = await this.seq.getMIDI();
+ const soundfont = new SoundFont2(mid.embeddedSoundFont || this.soundFont);
+ applySnapshotToMIDI(mid, await this.synth.getSynthesizerSnapshot());
+ trimSoundfont(soundfont, mid);
+ const binary = soundfont.write({compress: compressed, compressionQuality: quality});
+ const blob = new Blob([binary.buffer], {type: "audio/soundfont"});
+ let extension = soundfont.soundFontInfo["ifil"].split(".")[0] === "3" ? "sf3" : "sf2";
+ this.saveBlob(blob, `${soundfont.soundFontInfo['INAM'] || "unnamed"}.${extension}`);
+ SpessaSynthGroupEnd();
+ }
+ }
+ ],
+ 99999999,
+ true,
+ this.localeManager
+ );
}
\ No newline at end of file
diff --git a/src/website/manager/manager.js b/src/website/manager/manager.js
index 5a1a70e2..5d48a413 100644
--- a/src/website/manager/manager.js
+++ b/src/website/manager/manager.js
@@ -14,7 +14,7 @@ import { LocaleManager } from '../locale/locale_manager.js'
import { isMobile } from '../js/utils/is_mobile.js'
import { SpessaSynthInfo } from '../../spessasynth_lib/utils/loggin.js'
import { keybinds } from '../js/keybinds.js'
-import { _doExporWav, _exportWav } from './export_wav.js'
+import { _doExportAudioData, _exportAudioData } from './export_audio.js'
import { exportMidi } from './export_midi.js'
import { _exportSoundfont } from './export_soundfont.js'
import { exportSong } from './export_song.js'
@@ -51,7 +51,7 @@ class Manager
/**
* Creates a new midi user interface.
- * @param context {BaseAudioContext}
+ * @param context {AudioContext}
* @param soundFontBuffer {ArrayBuffer}
* @param locale {LocaleManager}
*/
@@ -324,8 +324,8 @@ class Manager
}
}
Manager.prototype.exportSong = exportSong;
-Manager.prototype._exportWav = _exportWav;
-Manager.prototype._doExportWav = _doExporWav;
+Manager.prototype._exportAudioData = _exportAudioData;
+Manager.prototype._doExportAudioData = _doExportAudioData;
Manager.prototype.exportMidi = exportMidi;
Manager.prototype._exportSoundfont = _exportSoundfont;
Manager.prototype._exportRMIDI = _exportRMIDI;