From 9ac93e38f18d1a389e786035f4f2440f39b7a268 Mon Sep 17 00:00:00 2001 From: hlaaftana Date: Mon, 25 Jun 2018 07:37:35 +0300 Subject: [PATCH] initial --- .gitignore | 4 ++ README.md | 31 +++++++++ nimedscript.nimble | 33 ++++++++++ src/nimedscript.nim | 153 ++++++++++++++++++++++++++++++++++++++++++++ tests/amp.nim | 20 ++++++ 5 files changed, 241 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 nimedscript.nimble create mode 100644 src/nimedscript.nim create mode 100644 tests/amp.nim diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0a6094e --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +bin/ +docs/ +*.edscript +*.js \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..8572464 --- /dev/null +++ b/README.md @@ -0,0 +1,31 @@ +# NimEdScript + +A wrapper around EdisonScript/JavaScript in FL Studio for Nim. + +It's important to note that EdisonScript's JS implementation is virtually lower than JavaScript 1.0, it lacks an enormous amount of features from the JavaScript language as it's mostly supposed to be a scripting language based around Pascal. The Nim JS output needs some transforming to get right, and until I've done something for that it's best to clone this repo, run nimble buildTests then copy the output in the bin folder to the FL studio scripts folder. + +List of things that do not work in edscript: + +* Math.trunc therefore vanilla Nim `mod` (hangs) +* The expression `typeof Int8Array` if Int8Array isn't defined, breaks nim default header even if unused +* Object default constructors like `var result = {a: 0, b: 0, c: 0, d: 0}; result = otherVar;` (slows it down a LOT, noinit doesn't change anything, use emit/importc/nodecl) +* Break/continue with labels, however labels and do/while loops are allowed +* Switch statement +* Any subscript access of any kind +* The identifier "base" whether variable or object key + +This needs the following post-codegen transforms: + +* Add `script "Script name" language "javascript"` to top of file +* 'base:' -> '"base":', if theres a var named base tough luck +* remove/comment out/simplify default nim primitive array header +* simply remove labels from labeled break/continue statements, this might break some block contraptions + +This makes the following impossible: + +* Anything that generates `nimCopy` + - Assigning object types to variables +* Anything that generates nim string/seq behaviour + - Use cstring and JS arrays + +Also to note, \ No newline at end of file diff --git a/nimedscript.nimble b/nimedscript.nimble new file mode 100644 index 0000000..dd88296 --- /dev/null +++ b/nimedscript.nimble @@ -0,0 +1,33 @@ +# Package + +version = "0.1.0" +author = "hlaaftana" +description = "FL Studio EdisonScript JS wrapper " +license = "MIT" +srcDir = "src" +skipDirs = @["tests"] + +# Dependencies + +requires "nim >= 0.18.0" + +task docs, "Build documentation": + exec "nim doc -o:docs/nimedscript.html src/nimedscript.nim" + +import ospaths, strformat, strutils + +proc transform(text: string): string = + # i would have done all of this better with regex or pegs or whatever but this'll have to do unless we hit a barrier + result = text + result = result.replace("if (typeof ", "//") # edscript doesn't like typeof undefined checking + result = result.replace("base:", "\"base\":") # it also doesn't like "base" as an identifier + result = result.replace("break ", "break; ") # it also doesn't like labeled break/continue + +task buildTests, "Compiles tests to javascript": + for test in listFiles("tests"): + let split = splitFile(test) + if split.ext == ".nim": + let path = "bin/" & split.name & ".edscript" + exec "nim js --gc:none -d:release -o:" & path & " " & test + writeFile(path, &"""script "{split.name}" language "javascript"; +{transform(readFile(path))}""") \ No newline at end of file diff --git a/src/nimedscript.nim b/src/nimedscript.nim new file mode 100644 index 0000000..240e3d3 --- /dev/null +++ b/src/nimedscript.nim @@ -0,0 +1,153 @@ +when not defined(JS): + {.error: "nimedscript only works in JS".} + +import jsffi + +type + single* = float32 + Sample* = single + + NormalizeFormatMode* = int + + PasteMode* {.pure.} = enum + ## What to do with the old audio when pasting + insert ## Move the old audio to after the pasted audio + replace ## Forget about the old audio and use the pasted audio instead + mix ## Play them both at the same time + + Region* {.pure, importc: "TRegion".} = object + sampleStart* {.importc: "SampleStart".}, sampleEnd* {.importc: "SampleEnd".}: int + name* {.importc: "Name".}, info* {.importc: "Info".}: cstring + time* {.importc: "Time".}: single + keyNum* {.importc: "KeyNum".}: int + + Sound* {.pure, importc: "TSample".} = object + length* {.importc: "Length".}, numChans* {.importc: "NumChans".}, + sampleRate* {.importc: "Samplerate".}, regionCount* {.importc: "RegionCount".}: int + + Editor* {.pure, importc: "TEditor".} = object + sound* {.importc: "Sample".}: Sound + + Input* {.pure, importc: "TInput".} = object + defaultValue* {.importc: "DefaultValue".}, value* {.importc: "Value".}: float + valueAsInt* {.importc: "ValueAsInt".}: int + min* {.importc: "Min".}, max* {.importc: "Max".}: float + + ScriptDialog* {.pure, importc: "TScriptDialog".} = object + +var + CRLF* {.importc, nodecl.}: cstring + ## \r\n convenience + editor* {.importc: "Editor", nodecl.}: Editor + ## Current Edison editor object + editorSample* {.importc: "EditorSample", nodecl.}: Sound + ## Sample currently in editor + scriptPath* {.importc: "ScriptPath", nodecl.}: cstring + ## Path of current script file + +proc createScriptDialog*(title, description: cstring): ScriptDialog {.importc: "CreateScriptDialog".} + ## Pops up a dialog in the editor with a title `title` and description `description` + ## and returns it as an object +proc progressMsg*(msg: cstring; pos, total: int) {.importc: "ProgressMsg".} + ## Shows a progress message `msg` with the progress bar being ``pos / total`` full +proc showMessage*(s: cstring) {.importc: "ShowMessage".} + ## Shows a message `s` on screen + +var + nfNumChannels* {.importc.}: NormalizeFormatMode + nfFormat* {.importc.}: NormalizeFormatMode + nfSamplerate* {.importc.}: NormalizeFormatMode + nfAll* {.importc.}: NormalizeFormatMode + +using + self: Region + +proc newRegion*(): Region {.importcpp: "TRegion(@)", constructor.} +proc copy*(self, source: Region) {.importcpp: "#.Copy(@)".} + +using + self: Sound + position, channel: int + x1, x2: int + vol: single + +proc newSound*(): Sound {.importcpp: "TSample(@)", constructor.} + ## Creates a new sample +proc getSampleAt*(self, position, channel): Sample {.importcpp: "#.GetSampleAt(@)".} + ## Gets individual sample (frame) of sound sample `self` at `position` in `channel` +proc setSampleAt*(self, position, channel; value: Sample) {.importcpp: "#.SetSampleAt(@)".} + ## Sets individual sample (frame) of sound sample `self` at `position` in `channel` to `value` + +template eachChannel*(self; body) {.dirty.} = + {.emit: ["var sound = ", self].} + var sound {.importc, nodecl.}: Sound + for chan {.noinit.} in 0 ..< sound.numChans: + var samples {.nodecl.}: distinct int + + template `[]`(sample: type(samples), position): Sample = + getSampleAt(sound, position, chan) + + template `[]=`(sample: type(samples), position; value: Sample) = + setSampleAt(sound, position, chan, value) + + body + +using self: Sound + +proc center*(self, x1, x2) {.importcpp: "#.CenterFromTo(@)".} + ## Centers audio between sample positions x1 and x2 +proc normalize*(self, x1, x2, vol, onlyIfAbove = false): float {.importcpp: "#.NormalizeFromTo(@)".} + ## Normalizes audio between sample positions x1 and x2 +proc amp*(self, x1, x2, vol) {.importcpp: "#.AmpFromTo(@)".} + ## Amplifies audio between sample positions x1 and x2 by volume vol +proc reverse*(self, x1, x2) {.importcpp: "#.ReverseFromTo(@)".} + ## Reverses audio between sample positions x1 and x2 +proc reversePolarity*(self, x1, x2) {.importcpp: "#.ReversePolarityFromTo(@)".} + ## Reverses polarity of audio between x1 and x2 +proc swapChannels*(self, x1, x2) {.importcpp: "#.SwapChannelsFromTo(@)".} +proc insertSilence*(self, x1, x2) {.importcpp: "#.InsertSilence(@)".} +proc silence*(self, x1, x2) {.importcpp: "#.SilenceFromTo(@)".} +proc noise*(self, x1, x2; mode = 1; vol = 1.0) {.importcpp: "#.NoiseFromTo(@)".} +proc sine*(self, x1, x2; freq, phase: float, vol = 1.0) {.importcpp: "#.SineFromTo(@)".} +proc paste*(self; aSound: Sound; x1, x2: int; mode = PasteMode.insert) {.importcpp: "#.PasteFromTo(@)".} +proc loadFromClipboard*(self) {.importcpp: "#.LoadFromClipboard(@)".} +proc delete*(self, x1, x2; copy = false) {.importcpp: "#.DeleteFromTo(@)".} +proc trim*(self, x1, x2) {.importcpp: "#.TrimFromTo(@)".} + +proc msToSamples*(self; time: float): Sample {.importcpp: "#.MsToSamples(@)".} +proc samplesToMs*(self; time: Sample): float {.importcpp: "#.SamplesToMs(@)".} + +proc loadFromFile*(self; filename: cstring) {.importcpp: "#.LoadFromFile(@)".} +proc loadFromFile_Ask*(self) {.importcpp: "#.LoadFromFile_Ask(@)".} +proc normalizeFormat*(self; source: Sound; mode: NormalizeFormatMode = nfAll) {.importcpp: "#.NormalizeFormat(@)".} +proc getRegion*(self; index: int): Region {.importcpp: "#.GetRegion(@)".} +proc addRegion*(self; name: cstring, sampleStart: int, sampleEnd = high(int)): int {.importcpp: "#.AddRegion(@)".} +proc deleteRegion*(self; index: int) {.importcpp: "#.DeleteRegion(@)".} + +using self: Editor + +proc getSelectionInSamples*(self; x1, x2: int): bool {.importcpp: "#.GetSelectionS(@)".} +proc getSelectionInMilliseconds*(self; x1, x2: float): bool {.importcpp: "#.GetSelectionMS(@)".} + +using self: ScriptDialog + +proc newScriptDialog*(): ScriptDialog {.importcpp: "TScriptDialog(@)", constructor} +proc addInput*(self; name: cstring, value: float): Input {.importcpp: "#.AddInput(@)".} +proc addInputKnob*(self; name: cstring, value, min, max: float): Input {.importcpp: "#.AddInputKnob(@)".} +proc addInputCombo*(self; name, valueList: cstring, value: int): Input {.importcpp: "#.AddInputCombo(@)".} +proc getInput*(self; name: cstring): Input {.importcpp: "#.GetInput(@)".} +proc getInputValue*(self; name: cstring): float {.importcpp: "#.GetInputValue(@)".} +proc getInputValueAsInt*(self; name: cstring): int {.importcpp: "#.GetInputValueAsInt(@)".} +proc execute*(self): bool {.importcpp: "#.Execute(@)".} + +proc `[]`*(self; name: cstring): Input {.inline.} = getInput(self, name) +proc `[]`*(self; name: cstring, T: typedesc[float]): T {.inline.} = getInputValue(self, name) +proc `[]`*(self; name: cstring; T: typedesc[int]): T {.inline.} = getInputValueAsInt(self, name) + +proc free*(self: Region | Sound | Editor | Input | ScriptDialog) {.importcpp: "#.Free()".} + +template `+`*(init: string | cstring, second: untyped): untyped = + bind toJs, to + to(toJs(cstring(init)) + toJs(second), cstring) + +proc `%`*[T](a, b: T): T {.importcpp: "# % #".} \ No newline at end of file diff --git a/tests/amp.nim b/tests/amp.nim new file mode 100644 index 0000000..3c55b2d --- /dev/null +++ b/tests/amp.nim @@ -0,0 +1,20 @@ +import ../src/nimedscript + +proc amp(value: float) = + var x1, x2 = 0 + discard editor.getSelectionInSamples(x1, x2) + + for n in x1..x2: + let x = n - x1 + if x % 10000 == 0: + progressMsg("Processing", x, x2 - x1) + eachChannel(editorSample): + samples[n] = Sample(samples[n].float * value) + +let form = createScriptDialog("Amp", "Simple amplification.") +try: + let volume = form.addInputKnob("Volume", 1, 0, 2) + if form.execute(): + amp(volume.value) +finally: + form.free() \ No newline at end of file