Repository: metrasynth/radiant-voices (language: python)

Issue:
Cannot load project with delay amount > 256
Reported from Telegram user:

> https://github.com/metrasynth/radiant-voices/blob/master/src/python/rv/modules/base/delay.py
> lines 32 & 33
>
> since we're able to specify delay in ms/hz (up to 8192),
> line 3 will generate an exception due to delay > 256 units

[issue-41-files.zip](https://github.com/metrasynth/radiant-voices/files/6062944/issue-41-files.zip)

Cannot load project with delay amount > 256
Reported from Telegram user:

> https://github.com/metrasynth/radiant-voices/blob/master/src/python/rv/modules/base/delay.py
> lines 32 & 33
>
> since we're able to specify delay in ms/hz (up to 8192),
> line 3 will generate an exception due to delay > 256 units

[issue-41-files.zip](https://github.com/metrasynth/radiant-voices/files/6062944/issue-41-files.zip)



Discussion / hints:

Fix patch:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index dd2b3a7..f74fcc5 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -2,6 +2,17 @@ Changelog
 =========
 
 
+1.0.1
+-----
+
+Bug fix release.
+
+Fixes (all versions)
+....................
+
+- Adds correct range information for delay controllers in Delay and Echo modules.
+
+
 1.0.0
 -----
 
diff --git a/specs/fileformat.yaml b/specs/fileformat.yaml
index 5879e0e..551eb7d 100644
--- a/specs/fileformat.yaml
+++ b/specs/fileformat.yaml
@@ -1012,8 +1012,28 @@ module_types:
     controllers:
       - dry: { min: 0, max: 512, default: 256 }
       - wet: { min: 0, max: 512, default: 256 }
-      - delay_l: { min: 0, max: 256, default: 128 }
-      - delay_r: { min: 0, max: 256, default: 160 }
+      - delay_l:
+          depends_on: delay_unit
+          default: 128
+          ranges:
+            sec/16384: { min: 0, max: 256 }
+            ms: { min: 0, max: 4000 }
+            hz: { min: 0, max: 8192 }
+            tick: { min: 0, max: 256 }
+            line: { min: 0, max: 256 }
+            line/2: { min: 0, max: 256 }
+            line/3: { min: 0, max: 256 }
+      - delay_r:
+          depends_on: delay_unit
+          default: 160
+          ranges:
+            sec/16384: { min: 0, max: 256 }
+            ms: { min: 0, max: 4000 }
+            hz: { min: 0, max: 8192 }
+            tick: { min: 0, max: 256 }
+            line: { min: 0, max: 256 }
+            line/2: { min: 0, max: 256 }
+            line/3: { min: 0, max: 256 }
       - volume_l: { min: 0, max: 256, default: 256 }
       - volume_r: { min: 0, max: 256, default: 256 }
       - channels: { enum: Channels, default: "stereo" }
@@ -1079,7 +1099,17 @@ module_types:
       - dry: { min: 0, max: 256, default: 256 }
       - wet: { min: 0, max: 256, default: 128 }
       - feedback: { min: 0, max: 256, default: 128 }
-      - delay: { min: 0, max: 256, default: 256 }
+      - delay:
+          depends_on: delay_unit
+          default: 256
+          ranges:
+            sec/256: { min: 0, max: 256 }
+            ms: { min: 0, max: 4000 }
+            hz: { min: 0, max: 8192 }
+            tick: { min: 0, max: 256 }
+            line: { min: 0, max: 256 }
+            line/2: { min: 0, max: 256 }
+            line/3: { min: 0, max: 256 }
       - channels: { enum: Channels, default: "stereo" }
       - delay_unit: { enum: DelayUnit, default: "sec/256" }
 
diff --git a/src/python/rv/modules/base/delay.py b/src/python/rv/modules/base/delay.py
index 8a32037..694cfb3 100644
--- a/src/python/rv/modules/base/delay.py
+++ b/src/python/rv/modules/base/delay.py
@@ -5,7 +5,7 @@
 """
 from enum import IntEnum
 
-from rv.controller import Controller
+from rv.controller import Controller, DependentRange, WarnOnlyRange
 
 
 class BaseDelay:
@@ -29,8 +29,38 @@ class DelayUnit(IntEnum):
 
     dry = Controller((0, 512), 256)
     wet = Controller((0, 512), 256)
-    delay_l = Controller((0, 256), 128)
-    delay_r = Controller((0, 256), 160)
+    delay_l = Controller(
+        DependentRange(
+            "delay_unit",
+            {
+                DelayUnit.sec_16384: WarnOnlyRange(0, 256),
+                DelayUnit.ms: WarnOnlyRange(0, 4000),
+                DelayUnit.hz: WarnOnlyRange(0, 8192),
+                DelayUnit.tick: WarnOnlyRange(0, 256),
+                DelayUnit.line: WarnOnlyRange(0, 256),
+                DelayUnit.line_2: WarnOnlyRange(0, 256),
+                DelayUnit.line_3: WarnOnlyRange(0, 256),
+            },
+            WarnOnlyRange(0, 256),
+        ),
+        128,
+    )
+    delay_r = Controller(
+        DependentRange(
+            "delay_unit",
+            {
+                DelayUnit.sec_16384: WarnOnlyRange(0, 256),
+                DelayUnit.ms: WarnOnlyRange(0, 4000),
+                DelayUnit.hz: WarnOnlyRange(0, 8192),
+                DelayUnit.tick: WarnOnlyRange(0, 256),
+                DelayUnit.line: WarnOnlyRange(0, 256),
+                DelayUnit.line_2: WarnOnlyRange(0, 256),
+                DelayUnit.line_3: WarnOnlyRange(0, 256),
+            },
+            WarnOnlyRange(0, 256),
+        ),
+        160,
+    )
     volume_l = Controller((0, 256), 256)
     volume_r = Controller((0, 256), 256)
     channels = Controller(Channels, Channels.stereo)
diff --git a/src/python/rv/modules/base/echo.py b/src/python/rv/modules/base/echo.py
index d6448e6..d435e8f 100644
--- a/src/python/rv/modules/base/echo.py
+++ b/src/python/rv/modules/base/echo.py
@@ -5,7 +5,7 @@
 """
 from enum import IntEnum
 
-from rv.controller import Controller
+from rv.controller import Controller, DependentRange, WarnOnlyRange
 
 
 class BaseEcho:
@@ -30,6 +30,21 @@ class DelayUnit(IntEnum):
     dry = Controller((0, 256), 256)
     wet = Controller((0, 256), 128)
     feedback = Controller((0, 256), 128)
-    delay = Controller((0, 256), 256)
+    delay = Controller(
+        DependentRange(
+            "delay_unit",
+            {
+                DelayUnit.sec_256: WarnOnlyRange(0, 256),
+                DelayUnit.ms: WarnOnlyRange(0, 4000),
+                DelayUnit.hz: WarnOnlyRange(0, 8192),
+                DelayUnit.tick: WarnOnlyRange(0, 256),
+                DelayUnit.line: WarnOnlyRange(0, 256),
+                DelayUnit.line_2: WarnOnlyRange(0, 256),
+                DelayUnit.line_3: WarnOnlyRange(0, 256),
+            },
+            WarnOnlyRange(0, 256),
+        ),
+        256,
+    )
     channels = Controller(Channels, Channels.stereo)
     delay_unit = Controller(DelayUnit, DelayUnit.sec_256)
diff --git a/src/ts/modtypes/delayBaseControllers.ts b/src/ts/modtypes/delayBaseControllers.ts
index bc12b78..19e6dc5 100644
--- a/src/ts/modtypes/delayBaseControllers.ts
+++ b/src/ts/modtypes/delayBaseControllers.ts
@@ -43,7 +43,29 @@ export class DelayBaseControllers implements Controllers {
   // noinspection JSUnusedGlobalSymbols
   set delayL(newValue: number) {
     const { controllerValues } = this
-    newValue = Math.min(Math.max(newValue, 0), 256)
+    switch (this.controllerValues.delayUnit) {
+      case DelayUnit.Sec_16384:
+        newValue = Math.min(Math.max(newValue, 0), 256)
+        break
+      case DelayUnit.Ms:
+        newValue = Math.min(Math.max(newValue, 0), 4000)
+        break
+      case DelayUnit.Hz:
+        newValue = Math.min(Math.max(newValue, 0), 8192)
+        break
+      case DelayUnit.Tick:
+        newValue = Math.min(Math.max(newValue, 0), 256)
+        break
+      case DelayUnit.Line:
+        newValue = Math.min(Math.max(newValue, 0), 256)
+        break
+      case DelayUnit.Line_2:
+        newValue = Math.min(Math.max(newValue, 0), 256)
+        break
+      case DelayUnit.Line_3:
+        newValue = Math.min(Math.max(newValue, 0), 256)
+        break
+    }
     controllerValues.delayL = newValue
   }
   // noinspection JSUnusedGlobalSymbols
@@ -53,7 +75,29 @@ export class DelayBaseControllers implements Controllers {
   // noinspection JSUnusedGlobalSymbols
   set delayR(newValue: number) {
     const { controllerValues } = this
-    newValue = Math.min(Math.max(newValue, 0), 256)
+    switch (this.controllerValues.delayUnit) {
+      case DelayUnit.Sec_16384:
+        newValue = Math.min(Math.max(newValue, 0), 256)
+        break
+      case DelayUnit.Ms:
+        newValue = Math.min(Math.max(newValue, 0), 4000)
+        break
+      case DelayUnit.Hz:
+        newValue = Math.min(Math.max(newValue, 0), 8192)
+        break
+      case DelayUnit.Tick:
+        newValue = Math.min(Math.max(newValue, 0), 256)
+        break
+      case DelayUnit.Line:
+        newValue = Math.min(Math.max(newValue, 0), 256)
+        break
+      case DelayUnit.Line_2:
+        newValue = Math.min(Math.max(newValue, 0), 256)
+        break
+      case DelayUnit.Line_3:
+        newValue = Math.min(Math.max(newValue, 0), 256)
+        break
+    }
     controllerValues.delayR = newValue
   }
   // noinspection JSUnusedGlobalSymbols
@@ -102,5 +146,7 @@ export class DelayBaseControllers implements Controllers {
   set delayUnit(newValue: DelayUnit) {
     const { controllerValues } = this
     controllerValues.delayUnit = newValue
+    this.delayL = this.delayL
+    this.delayR = this.delayR
   }
 }
diff --git a/src/ts/modtypes/echoBaseControllers.ts b/src/ts/modtypes/echoBaseControllers.ts
index 6368864..d72bd5a 100644
--- a/src/ts/modtypes/echoBaseControllers.ts
+++ b/src/ts/modtypes/echoBaseControllers.ts
@@ -53,7 +53,29 @@ export class EchoBaseControllers implements Controllers {
   // noinspection JSUnusedGlobalSymbols
   set delay(newValue: number) {
     const { controllerValues } = this
-    newValue = Math.min(Math.max(newValue, 0), 256)
+    switch (this.controllerValues.delayUnit) {
+      case DelayUnit.Sec_256:
+        newValue = Math.min(Math.max(newValue, 0), 256)
+        break
+      case DelayUnit.Ms:
+        newValue = Math.min(Math.max(newValue, 0), 4000)
+        break
+      case DelayUnit.Hz:
+        newValue = Math.min(Math.max(newValue, 0), 8192)
+        break
+      case DelayUnit.Tick:
+        newValue = Math.min(Math.max(newValue, 0), 256)
+        break
+      case DelayUnit.Line:
+        newValue = Math.min(Math.max(newValue, 0), 256)
+        break
+      case DelayUnit.Line_2:
+        newValue = Math.min(Math.max(newValue, 0), 256)
+        break
+      case DelayUnit.Line_3:
+        newValue = Math.min(Math.max(newValue, 0), 256)
+        break
+    }
     controllerValues.delay = newValue
   }
   // noinspection JSUnusedGlobalSymbols
@@ -73,5 +95,6 @@ export class EchoBaseControllers implements Controllers {
   set delayUnit(newValue: DelayUnit) {
     const { controllerValues } = this
     controllerValues.delayUnit = newValue
+    this.delay = this.delay
   }
 }


Test patch:
diff --git a/tests/files/issue41/sample.sunvox b/tests/files/issue41/sample.sunvox
new file mode 100644
index 0000000..3bfc73a
Binary files /dev/null and b/tests/files/issue41/sample.sunvox differ
diff --git a/tests/python/readwrite/conftest.py b/tests/python/conftest.py
similarity index 94%
rename from tests/python/readwrite/conftest.py
rename to tests/python/conftest.py
index 9cc4c05..da25823 100644
--- a/tests/python/readwrite/conftest.py
+++ b/tests/python/conftest.py
@@ -7,7 +7,7 @@
 
 @pytest.fixture
 def test_files_path() -> Path:
-    return Path(__file__).parent / "../../files"
+    return Path(__file__).parent / "../files"
 
 
 @pytest.fixture
diff --git a/tests/python/issues/__init__.py b/tests/python/issues/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/python/issues/test_issue41.py b/tests/python/issues/test_issue41.py
new file mode 100644
index 0000000..e987d41
--- /dev/null
+++ b/tests/python/issues/test_issue41.py
@@ -0,0 +1,9 @@
+from rv.api import m
+
+
+def test_issue41(read_write_read_project):
+    project = read_write_read_project("issue41/sample")
+    delay: m.Delay = project.modules[1]
+    assert delay.delay_l == 8192
+    assert delay.delay_r == 8192
+    assert delay.delay_unit == delay.DelayUnit.hz
diff --git a/tests/ts/issues/issue41.test.ts b/tests/ts/issues/issue41.test.ts
new file mode 100644
index 0000000..02be56f
--- /dev/null
+++ b/tests/ts/issues/issue41.test.ts
@@ -0,0 +1,30 @@
+import { Project } from "@radiant-voices/project"
+import { readFileSync } from "fs"
+import { fromIffBuffer } from "@radiant-voices/chunks/fromIffBuffer"
+import { readSunVoxFile } from "@radiant-voices/reader/readSunVoxFile"
+import { m } from "@radiant-voices/modtypes"
+import { objectChunks } from "@radiant-voices/writer/objectChunks"
+import { toIffBuffer } from "@radiant-voices/chunks/toIffBuffer"
+
+describe("Reading issue41/sample.sunvox file", () => {
+  const filePath = "tests/files/issue41/sample.sunvox"
+  let project: Project
+  beforeAll(() => {
+    // read, write, read
+    const f = readFileSync(filePath)
+    project = readSunVoxFile(fromIffBuffer(f)) as Project
+    const f2 = toIffBuffer(objectChunks(project))
+    project = readSunVoxFile(fromIffBuffer(f2)) as Project
+  })
+  test("has expected delay values", () => {
+    const delay = project.modules[1] as m.Delay.Module
+    const { c } = delay
+    expect(c.delayL).toEqual(8192)
+    expect(c.delayR).toEqual(8192)
+    expect(c.delayUnit).toEqual(m.Delay.DelayUnit.Hz)
+    c.delayL = 7777
+    c.delayR = 6666
+    expect(c.delayL).toEqual(7777)
+    expect(c.delayR).toEqual(6666)
+  })
+})