575 lines
23 KiB
Python
575 lines
23 KiB
Python
"""GUI smoke tests: the editor driven headless (offscreen Qt, synthetic game, synchronous jobs)."""
|
|
import hashlib
|
|
import os
|
|
|
|
import numpy as np
|
|
import pytest
|
|
|
|
pytest.importorskip("PySide6")
|
|
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
|
os.environ["ACKIT_NO_AUDIO"] = "1"
|
|
|
|
from PySide6.QtTest import QTest # noqa: E402
|
|
from PySide6.QtWidgets import QApplication # noqa: E402
|
|
|
|
from ackit import audio, workspace as W # noqa: E402
|
|
from ackit.inject import inject # noqa: E402
|
|
import synth # noqa: E402
|
|
|
|
pytestmark = pytest.mark.gui
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def qapp():
|
|
return QApplication.instance() or QApplication([])
|
|
|
|
|
|
def files(root):
|
|
out = {}
|
|
for d, _, fs in os.walk(os.path.join(root, "BmGame")):
|
|
for f in fs:
|
|
p = os.path.join(d, f)
|
|
out[p] = hashlib.sha1(open(p, "rb").read()).hexdigest()
|
|
return out
|
|
|
|
|
|
@pytest.fixture
|
|
def win(qapp, synth_game, tmp_path):
|
|
from app.mainwindow import MainWindow
|
|
from app.workers import Runner
|
|
root, originals = synth_game
|
|
w = MainWindow(settings={"full_backup": True, "language": "INT"}, runner=Runner(sync=True), game_dir=root)
|
|
w.confirm = lambda *a: True
|
|
w.inform = lambda *a: None
|
|
w.editor.confirm = lambda *a: True
|
|
w.editor.inform = lambda title, text: (_ for _ in ()).throw(AssertionError("%s: %s" % (title, text)))
|
|
QTest.qWait(50)
|
|
assert w.ws is not None, w.banner_text.text()
|
|
yield w, root, originals
|
|
w.close()
|
|
|
|
|
|
def type_search(w, text):
|
|
w.search.setText(text)
|
|
w._filter_text()
|
|
|
|
|
|
def edit(w, name):
|
|
"""Select a sound in the Sounds tab and open it in the Edit tab (what Enter / the Edit button do)."""
|
|
assert w.select_name(name)
|
|
w.edit_selected()
|
|
assert w.tabs.currentWidget() is w.edit and w.editor.sound.name == name
|
|
return w.editor
|
|
|
|
|
|
def test_browse_search_filter(win):
|
|
w, root, _ = win
|
|
assert w.model.rowCount() == len(w.ws.catalog.sounds)
|
|
type_search(w, "synth radio 02")
|
|
assert w.proxy.rowCount() == 1
|
|
type_search(w, "")
|
|
w.cmb_group.setCurrentText("Music")
|
|
assert w.proxy.rowCount() == 3 and all(w.proxy.sound_at(w.proxy.index(r, 0)).group == "Music"
|
|
for r in range(w.proxy.rowCount()))
|
|
w.clear_filters()
|
|
assert w.proxy.rowCount() == w.model.rowCount()
|
|
|
|
|
|
def test_sort_order_survives_filter_changes(win):
|
|
"""Rows that a filter shows again go to their sorted place (not appended at the end)."""
|
|
from PySide6.QtCore import Qt
|
|
from app.models import C_NAME, SORT_ROLE
|
|
w, _, _ = win
|
|
|
|
def keys():
|
|
return [w.proxy.data(w.proxy.index(r, C_NAME), SORT_ROLE) for r in range(w.proxy.rowCount())]
|
|
|
|
w.table.sortByColumn(C_NAME, Qt.SortOrder.AscendingOrder)
|
|
assert keys() == sorted(keys())
|
|
w.cmb_group.setCurrentText("Music")
|
|
w.clear_filters()
|
|
type_search(w, "radio")
|
|
type_search(w, "")
|
|
assert w.proxy.rowCount() == w.model.rowCount() and keys() == sorted(keys())
|
|
w.table.sortByColumn(C_NAME, Qt.SortOrder.DescendingOrder)
|
|
w.cmb_group.setCurrentText("Speech")
|
|
w.clear_filters()
|
|
assert keys() == sorted(keys(), reverse=True)
|
|
|
|
|
|
def test_replace_apply_revert_byte_identical(win, tmp_path):
|
|
w, root, originals = win
|
|
before = files(root)
|
|
type_search(w, "Speech_Synth_Radio_02")
|
|
ed = edit(w, "Speech_Synth_Radio_02")
|
|
assert ed.orig is not None
|
|
wav = str(tmp_path / "new.wav")
|
|
x, sr = synth.tone(1.0, 44100, 990)
|
|
audio.save_wav(wav, x, sr)
|
|
assert ed.load_file(wav)
|
|
ed.cb_match.setChecked(True)
|
|
ed._update_new()
|
|
assert ed.processed() is not None
|
|
ed.apply()
|
|
s = ed.sound
|
|
assert w.ws.state(s) == W.MODIFIED and s.key in w.model.modified
|
|
assert files(root) != before
|
|
w.cb_mod.setChecked(True)
|
|
assert w.proxy.rowCount() == 1
|
|
ed.revert()
|
|
assert files(root) == before
|
|
assert w.ws.state(s) == W.ORIGINAL and not w.model.modified
|
|
# undo of the original is impossible to lose: the store is empty again
|
|
assert not w.ws.store.entries
|
|
|
|
|
|
def test_bank_sound_apply_revert_in_editor(win, tmp_path):
|
|
w, root, originals = win
|
|
before = files(root)
|
|
w.cmb_arch.setCurrentText("BANK")
|
|
assert w.proxy.rowCount() == 7 # 6 neutral + the INT copy of a localized line
|
|
ed = edit(w, "CHR_SynthFoley_1")
|
|
assert "bank CHR_SynthFoley (2 copies)" in ed.meta.text()
|
|
wav = str(tmp_path / "foley.wav")
|
|
audio.save_wav(wav, *synth.tone(0.2, 44100, 1300))
|
|
assert ed.load_file(wav)
|
|
ed._update_new()
|
|
ed.apply()
|
|
assert w.ws.state(ed.sound) == W.MODIFIED and files(root) != before
|
|
ed.revert()
|
|
assert files(root) == before and not w.ws.store.entries
|
|
|
|
|
|
def test_subtitles_tab_edit_and_link(win):
|
|
w, root, _ = win
|
|
before = files(root)
|
|
w.tabs.setCurrentWidget(w.subs)
|
|
p = w.subs
|
|
assert p.lang == "INT" and p.model.rowCount() == 3
|
|
p.search.setText("scanner")
|
|
p._filter()
|
|
assert p.proxy.rowCount() == 1
|
|
p.table.selectRow(0)
|
|
assert p.edit.text() == "Scanner line two." and "/ 17 characters" in p.lbl_info.text()
|
|
p.edit.setText("x" * 30)
|
|
assert not p.b_apply.isEnabled() # longer than the room
|
|
p.edit.setText("Scan two.")
|
|
p.apply_current()
|
|
key = p.current.key
|
|
assert w.ws.texts.state(key) == "modified" and p.model.modified[key] == "Scan two."
|
|
p.go_to_sound.emit(p.current.sound) # "Go to the sound"
|
|
assert w.tabs.currentIndex() == 0 and w.preview.sound.name == "Speech_Synth_Radio_02"
|
|
assert "Scan two." in w.preview.sub.text() # the browser shows it, read-only
|
|
w.edit_selected()
|
|
box = w.editor.subtitle
|
|
assert [e.text() for e in box.edits] == ["Scan two.", "Second part."]
|
|
box.edits[1].setText("Part two.")
|
|
box.apply()
|
|
assert w.ws.texts.current(key.replace(":0", ":1")) == "Part two."
|
|
box.open_in_list.emit(box.keys[1]) # back to the Subtitles tab
|
|
assert w.tabs.currentWidget() is p and p.current.key == box.keys[1]
|
|
box.revert()
|
|
assert files(root) == before and not w.ws.texts.records()
|
|
|
|
|
|
def test_one_language_for_everything(win):
|
|
w, root, _ = win
|
|
assert w.language == "INT" and w.cmb_lang.currentText() == "INT" and not w.lbl_voices.text()
|
|
assert {"INT", "ITA", "RUS"} <= {w.cmb_lang.itemText(i) for i in range(w.cmb_lang.count())}
|
|
w.load_game(root, "RUS") # no Russian voices: INT speech, RUS texts
|
|
assert w.language == "RUS" and "voices: INT" in w.lbl_voices.text()
|
|
assert "INT" in w.ws.catalog.archives
|
|
w.tabs.setCurrentWidget(w.subs)
|
|
assert w.subs.lang == "RUS" and "RUS" in w.subs.lbl_lang.text()
|
|
w.tabs.setCurrentIndex(0)
|
|
edit(w, "Speech_Synth_Radio_02")
|
|
assert [e.text() for e in w.editor.subtitle.edits] == ["Строка два"]
|
|
w.tabs.setCurrentWidget(w.menu)
|
|
assert w.menu.lang is None and w.menu.model.rowCount() == 0 # no RUS menu files in the synthetic game
|
|
|
|
|
|
def test_subtitles_find_replace(win):
|
|
w, root, _ = win
|
|
before = files(root)
|
|
w.tabs.setCurrentWidget(w.subs)
|
|
p = w.subs
|
|
assert p.find_replace("line", "row") == 1
|
|
assert w.ws.texts.current("SUB:INT:%s:0" % synth.SPEECH_EVENT) == "Scanner row two."
|
|
p.revert_all()
|
|
assert files(root) == before
|
|
|
|
|
|
def test_menu_text_tab(win):
|
|
w, root, _ = win
|
|
before = files(root)
|
|
w.tabs.setCurrentWidget(w.menu)
|
|
p = w.menu
|
|
assert p.model.rowCount() == 4
|
|
p.filters["File"].setCurrentText("FRONTEND.int")
|
|
p.search.setText("press")
|
|
p._filter()
|
|
assert p.proxy.rowCount() == 1
|
|
p.table.selectRow(0)
|
|
p.edit.setText("Press Enter to start")
|
|
p.apply_current()
|
|
assert w.ws.texts.current("MENU:INT:FRONTEND.int:Menu:Start") == "Press Enter to start"
|
|
p.cb_mod.setChecked(True)
|
|
assert p.proxy.rowCount() == 1
|
|
p.revert_all()
|
|
assert files(root) == before
|
|
|
|
|
|
def test_mod_with_texts(win, tmp_path):
|
|
w, root, _ = win
|
|
tx = w.ws.texts
|
|
tx.apply({"SUB:INT:%s:0" % synth.SPEECH_EVENT: "Mod line.", "MENU:INT:FRONTEND.int:Menu:Quit": "Leave"})
|
|
pkg = str(tmp_path / "texts.acmod")
|
|
w.export_mod_dialog(path=pkg, keys=tx.modified(), title="texts")
|
|
tx.revert_all()
|
|
w.import_mod_dialog(path=pkg, confirm=False)
|
|
assert tx.current("MENU:INT:FRONTEND.int:Menu:Quit") == "Leave"
|
|
assert tx.current("SUB:INT:%s:0" % synth.SPEECH_EVENT) == "Mod line."
|
|
tx.revert_all()
|
|
|
|
|
|
def test_revert_all_reverts_sounds_and_texts(win):
|
|
w, root, _ = win
|
|
before = files(root)
|
|
tx = w.ws.texts
|
|
tx.apply({"SUB:INT:%s:0" % synth.SPEECH_EVENT: "Undo me.", "MENU:INT:FRONTEND.int:Menu:Quit": "Leave"})
|
|
s = w.ws.sound("CHR_RadioScanner_1")
|
|
w.ws.apply([(s, synth.wem(0.5, 1200), {})])
|
|
w.revert_all()
|
|
assert files(root) == before and not w.ws.store.entries and not tx.records()
|
|
|
|
|
|
def test_mod_import_asks_before_overwriting(win, tmp_path):
|
|
"""Files changed outside the editor are overwritten by a mod only when the user confirms."""
|
|
w, root, originals = win
|
|
s = w.ws.sound("CHR_RadioScanner_1")
|
|
w.ws.apply([(s, synth.wem(0.5, 1200), {})])
|
|
pkg = str(tmp_path / "m.acmod")
|
|
w.export_mod_dialog(path=pkg, keys=[s.key], title="m")
|
|
other = synth.wem(0.4, 150)
|
|
inject(w.ws.wad_path(s), {s.target: other}, {s.target: w.ws.limit(s)}) # another tool writes the slot
|
|
asked = []
|
|
w.confirm = lambda *a: asked.append(a) or False
|
|
w.import_mod_dialog(path=pkg, confirm=False)
|
|
assert asked and w.ws.current_bytes(s) == other # declined: left alone
|
|
w.confirm = lambda *a: True
|
|
w.import_mod_dialog(path=pkg, confirm=False)
|
|
assert w.ws.state(s) == W.MODIFIED
|
|
w.revert_all()
|
|
|
|
|
|
def test_store_cannot_change_while_changes_are_applied(win, tmp_path, monkeypatch):
|
|
from app import mainwindow
|
|
w, root, _ = win
|
|
s = w.ws.sound("CHR_RadioScanner_1")
|
|
w.ws.apply([(s, synth.wem(0.5, 1200), {})])
|
|
store = w.ws.store.root
|
|
|
|
class Dlg:
|
|
def __init__(self, settings, parent):
|
|
pass
|
|
|
|
def exec(self):
|
|
return mainwindow.QDialog.DialogCode.Accepted
|
|
|
|
def values(self):
|
|
return dict(w.settings, store_root=str(tmp_path / "elsewhere"))
|
|
monkeypatch.setattr(mainwindow, "SettingsDialog", Dlg)
|
|
told = []
|
|
w.inform = lambda *a: told.append(a)
|
|
w.settings_dialog()
|
|
assert told and w.settings.get("store_root") != str(tmp_path / "elsewhere") and w.ws.store.root == store
|
|
w.revert_all()
|
|
|
|
|
|
def test_text_undone_outside_shows_banner(win):
|
|
w, root, _ = win
|
|
tx = w.ws.texts
|
|
k = "MENU:INT:FRONTEND.int:Menu:Start"
|
|
tx.apply({k: "Go"})
|
|
path = tx.files(k)[0]
|
|
open(path, "wb").write(open(w.ws.store.full_copy(w.ws.game.rel(path)), "rb").read()) # like 'verify files'
|
|
w.check_problems()
|
|
assert not w.banner.isHidden() and "text changes" in w.banner_text.text()
|
|
w.reapply_lost()
|
|
assert tx.current(k) == "Go" and tx.state(k) == "modified"
|
|
tx.revert_all()
|
|
|
|
|
|
def test_record_simulated_input(win):
|
|
w, root, _ = win
|
|
ed = edit(w, "CHR_RadioScanner_1")
|
|
x, sr = synth.tone(0.5, 48000, 500)
|
|
ed.recorder.start_simulated(x, sr, realtime=True)
|
|
assert ed.recorder.is_recording()
|
|
QTest.qWait(800)
|
|
assert ed.meter.value() > 0
|
|
ed.toggle_record() # stop -> becomes the replacement source
|
|
assert ed.src is not None and abs(len(ed.src[0]) - len(x)) <= sr // 50
|
|
ed.trim_silence()
|
|
ed.apply()
|
|
assert w.ws.state(ed.sound) == W.MODIFIED
|
|
w.revert_all()
|
|
assert not w.ws.modified_keys()
|
|
|
|
|
|
def test_notes_tags_favorites(win):
|
|
w, root, _ = win
|
|
w.select_name("CHR_RadioScanner_3")
|
|
s = w.selected_sounds()[0]
|
|
w.edit_notes([s], note="needs a new line", tags=["todo", "radio"])
|
|
assert w.userdata.tags(s.key) == ["radio", "todo"]
|
|
w.cmb_tag.setCurrentText("todo")
|
|
assert w.proxy.rowCount() == 1
|
|
w.cmb_tag.setCurrentIndex(0)
|
|
w._toggle_fav([s])
|
|
w.cb_fav.setChecked(True)
|
|
assert w.proxy.rowCount() == 1
|
|
type_search(w, "needs a new")
|
|
assert w.proxy.rowCount() == 1
|
|
|
|
|
|
def test_mod_export_import(win, tmp_path):
|
|
w, root, _ = win
|
|
edit(w, "Speech_Synth_Radio_02")
|
|
x, sr = synth.tone(1.0, 24000, 1200)
|
|
w.editor.set_source(x, sr, "tone")
|
|
w.editor.apply()
|
|
key = w.editor.sound.key
|
|
pkg = str(tmp_path / "pkg.acmod")
|
|
w.export_mod_dialog(path=pkg, keys=[key], title="test")
|
|
assert os.path.exists(pkg)
|
|
w.revert_all()
|
|
assert not w.ws.modified_keys()
|
|
w.import_mod_dialog(path=pkg, confirm=False)
|
|
assert w.ws.modified_keys() == [key]
|
|
w.revert_all()
|
|
|
|
|
|
def test_changed_underneath_banner(win):
|
|
w, root, originals = win
|
|
edit(w, "CHR_RadioScanner_1")
|
|
x, sr = synth.tone(0.5, 24000, 1500)
|
|
w.editor.set_source(x, sr, "tone")
|
|
w.editor.apply()
|
|
s = w.editor.sound
|
|
# Steam "verify files" puts the original back behind our back
|
|
inject(w.ws.wad_path(s), {s.target: originals[s.target]}, {s.target: w.ws.limit(s)})
|
|
w.check_problems()
|
|
assert not w.banner.isHidden() and "undone outside" in w.banner_text.text()
|
|
w.reapply_lost()
|
|
assert w.ws.state(s) == W.MODIFIED
|
|
w.revert_all()
|
|
|
|
|
|
class HeldRunner:
|
|
"""Keeps jobs pending until release(), to see the loading state."""
|
|
|
|
def __init__(self, real):
|
|
self.real, self.held = real, []
|
|
|
|
def start(self, *args):
|
|
self.held.append(args)
|
|
|
|
def release(self):
|
|
while self.held:
|
|
self.real.start(*self.held.pop(0))
|
|
|
|
|
|
def test_selection_survives_sort_and_filter(win):
|
|
from PySide6.QtCore import Qt
|
|
from app.models import C_LEN
|
|
w, _, _ = win
|
|
assert w.select_name("Speech_Synth_Radio_02")
|
|
s = w.preview.sound
|
|
w.table.sortByColumn(C_LEN, Qt.SortOrder.DescendingOrder)
|
|
type_search(w, "radio")
|
|
cur = w.table.currentIndex()
|
|
assert cur.isValid() and w.proxy.sound_at(cur) is s and [x.key for x in w.selected_sounds()] == [s.key]
|
|
type_search(w, "music") # the row is filtered out: no current row
|
|
assert not w.table.currentIndex().isValid() and not w.selected_sounds()
|
|
|
|
|
|
def test_list_spinners_while_loading(win):
|
|
"""The sound list and the text lists show the loading overlay while they load (background jobs); a subtitle
|
|
asked for meanwhile is selected when the rows arrive."""
|
|
from ackit import subtitles
|
|
w, root, _ = win
|
|
held = w.runner = HeldRunner(w.runner)
|
|
w.load_game(root, "INT")
|
|
assert w.table_busy.busy and w.table_busy.timer.isActive()
|
|
held.release()
|
|
assert not w.table_busy.busy and w.model.rowCount() == len(w.ws.catalog.sounds)
|
|
w._text_loaded.clear()
|
|
sub = w.ws.texts.subtitles("INT")[0]
|
|
key = subtitles.key("INT", sub.speech, sub.line)
|
|
w.show_subtitle(key)
|
|
assert w.subs.busy.busy and w.subs.busy.timer.isActive()
|
|
held.release()
|
|
assert not w.subs.busy.busy and w.subs.current is not None and w.subs.current.key == key
|
|
|
|
|
|
def test_spinners_while_loading(win, tmp_path):
|
|
w, root, _ = win
|
|
held = w.runner = HeldRunner(w.runner)
|
|
w.select_name("CHR_RadioScanner_1")
|
|
assert w.preview.wf_orig.busy and w.preview.wf_orig._timer.isActive()
|
|
held.release()
|
|
assert not w.preview.wf_orig.busy and not w.preview.wf_orig._timer.isActive()
|
|
w.edit_selected()
|
|
assert w.editor.wf_orig.busy
|
|
held.release()
|
|
assert not w.editor.wf_orig.busy
|
|
wav = str(tmp_path / "a.wav")
|
|
audio.save_wav(wav, *synth.tone(0.3, 22050, 700))
|
|
w.editor.load_file(wav)
|
|
assert w.editor.wf_new.busy
|
|
held.release()
|
|
assert not w.editor.wf_new.busy and w.editor.src is not None
|
|
w._run_change("Working…", lambda job: None, "done")
|
|
assert not w.spinner.isHidden()
|
|
held.release()
|
|
assert w.spinner.isHidden()
|
|
w.runner = held.real
|
|
|
|
|
|
def test_background_toggle_and_filters_look_alike(win):
|
|
w, root, _ = win
|
|
w.sidebar.configure({"background_enabled": False})
|
|
assert w.sidebar.image is None
|
|
# every filter selector is the same kind of plain drop-down
|
|
assert not any(c.isEditable() for c in (w.cmb_group, w.cmb_cat, w.cmb_char, w.cmb_arch, w.cmb_tag))
|
|
|
|
|
|
def test_browser_only_browses_edit_tab_fits(win):
|
|
"""The Sounds tab has no editing tools (player + Edit); the Edit tab needs no scrolling at 1400 x 900 and
|
|
1280 x 800, even for a speech line with a two-part subtitle."""
|
|
from PySide6.QtWidgets import QPushButton
|
|
w, root, _ = win
|
|
texts = {b.text() for b in w.vsplit.findChildren(QPushButton)}
|
|
assert {"Edit", "Add to edit list", "Play original", "Play installed", "Stop"} <= texts
|
|
assert not texts & {"Record", "Apply to game", "Load file…", "Revert"}
|
|
w.show()
|
|
for size in ((1400, 900), (1280, 800)):
|
|
w.resize(*size)
|
|
edit(w, "Speech_Synth_Radio_02") # two subtitle parts: the tallest case
|
|
QTest.qWait(100)
|
|
assert w.editor.subtitle_box.isVisible() and len(w.editor.subtitle.edits) == 2
|
|
for area in (w.editor.parentWidget().parentWidget(), w.edit.rack.parentWidget().parentWidget()):
|
|
assert not area.verticalScrollBar().isVisible() and not area.horizontalScrollBar().isVisible(), size
|
|
edit(w, "CHR_SynthFoley_1")
|
|
assert not w.editor.subtitle_box.isVisible() # no subtitle: no subtitle box
|
|
|
|
|
|
def test_recorder_sample_formats(qapp):
|
|
"""Input devices may deliver 16/32-bit integer, 8-bit or float samples, and reads can split a frame."""
|
|
import numpy as np
|
|
from app.audioio import Recorder
|
|
r = Recorder()
|
|
x = np.array([[0.5, -0.25], [0.125, -1.0]], np.float32)
|
|
for sample, raw in (("<i2", (x * 32768).astype("<i2").tobytes()), ("<i4", (x * 2147483648.0).astype("<i4").tobytes()),
|
|
("u1", (x * 128 + 128).astype("u1").tobytes()), ("<f4", x.astype("<f4").tobytes())):
|
|
r.chunks, r._rest, r.channels, r.sample = [], b"", 2, sample
|
|
r._ingest(raw[:3])
|
|
r._ingest(raw[3:])
|
|
got, _ = r.stop()
|
|
assert np.allclose(got, x, atol=1 / 128), sample
|
|
|
|
|
|
def test_edit_list_replacement_effects_apply_revert(win, tmp_path):
|
|
"""Edit tab: sounds come from the browser onto the edit list; a replacement and effect settings are kept per sound
|
|
(user data), applied with the effects, reverted byte-identically; Previous / Next walk the list."""
|
|
from ackit import fx
|
|
w, root, _ = win
|
|
e = w.edit
|
|
before = files(root)
|
|
a, b = w.ws.sound("Speech_Synth_Radio_02"), w.ws.sound("Speech_TAPE_JOKER_1")
|
|
w.edit_sounds([a, b])
|
|
assert w.tabs.currentWidget() is e and [s.key for s in e.model.sounds] == [a.key, b.key]
|
|
ed = w.editor
|
|
assert ed.sound is a and not ed.b_prev.isEnabled() and ed.b_next.isEnabled()
|
|
assert ed.b_apply.isEnabled() and ed.b_export.isEnabled() and ed.b_rec.isEnabled()
|
|
wav = str(tmp_path / "new.wav")
|
|
audio.save_wav(wav, *synth.tone(1.2, 44100, 440))
|
|
assert ed.load_file(wav)
|
|
assert e.store.has_replacement(a.key) and e.model.state(a) == "Replacement saved"
|
|
dry = ed.processed()[0].copy()
|
|
e.rack.apply_look("Radio") # EQ, distortion, static
|
|
assert fx.summary(ed.effects) == "Band-pass EQ, Distortion, Noise"
|
|
wet = ed.processed()[0]
|
|
assert not np.allclose(wet[:len(dry)], dry, atol=1e-3)
|
|
e.flush()
|
|
assert e.store.get(a.key)["fx"]["eq"]["on"] is True
|
|
ed.apply()
|
|
assert w.ws.state(a) == W.MODIFIED and w.ws.store.get(a.key)["meta"]["effects"] == fx.summary(ed.effects)
|
|
assert a.key in w.model.modified and e.model.state(a) == "Applied"
|
|
QTest.qWait(50)
|
|
ed.b_next.click() # the next sound: no replacement, no effects
|
|
assert ed.sound is b and ed.src is None and not e.rack.is_on("eq") and ed.b_prev.isEnabled()
|
|
ed.b_prev.click() # back: replacement, values and effects return
|
|
assert ed.sound is a and ed.src is not None and e.rack.is_on("eq") and ed.cb_match.isChecked()
|
|
ed.revert()
|
|
QTest.qWait(50)
|
|
assert files(root) == before and not w.ws.store.entries
|
|
assert e.store.has_replacement(a.key) # the replacement stays for later
|
|
e.table.selectRow(1)
|
|
e.remove_selected()
|
|
assert [s.key for s in e.model.sounds] == [a.key] and e.store.list == [a.key]
|
|
|
|
|
|
def test_effects_panel_looks_presets_sliders(win):
|
|
from PySide6.QtCore import QItemSelectionModel
|
|
from ackit import fx
|
|
w, _, _ = win
|
|
w.edit_sounds([w.ws.sound("CHR_RadioScanner_1"), w.ws.sound("CHR_RadioScanner_3")])
|
|
rack = w.edit.rack
|
|
rack.apply_preset("reverb", "Hall")
|
|
s = rack.settings()
|
|
assert s["reverb"]["on"] and s["reverb"]["decay"] == 2.0 and rack.stack.currentWidget() is rack.pages["reverb"]
|
|
rack.show_block("echo")
|
|
rack.controls[("echo", "time")].slider.setValue(100) # moving a slider turns its effect on
|
|
assert rack.settings()["echo"]["on"] and rack.items["echo"].font().bold()
|
|
rack.set_on("echo", False)
|
|
assert not rack.settings()["echo"]["on"]
|
|
c = rack.controls[("eq", "low_cut")] # log slider, readable value
|
|
c.slider.setValue(c.slider.maximum())
|
|
assert rack.settings()["eq"]["low_cut"] == 2000 and c.value_lbl.text() == "2.0 kHz"
|
|
c.slider.reset.emit() # double-click: back to the default
|
|
assert rack.settings()["eq"]["low_cut"] == 300
|
|
rack.cmb_preset.activated.emit(rack.cmb_preset.findText("PA announcement")) # the preset list
|
|
assert rack.cmb_preset.currentIndex() == 0
|
|
s = rack.settings()
|
|
assert [b.key for b in fx.BLOCKS if s[b.key]["on"]] == ["eq", "echo", "channels"] and s["echo"]["time"] == 160
|
|
rack.b_copy.click()
|
|
rack.b_reset.click()
|
|
assert not fx.active(rack.settings())
|
|
rack.b_paste.click()
|
|
assert rack.settings()["echo"]["time"] == 160
|
|
sm = w.edit.table.selectionModel()
|
|
for r in range(w.edit.model.rowCount()):
|
|
sm.select(w.edit.model.index(r, 0), QItemSelectionModel.SelectionFlag.Select |
|
|
QItemSelectionModel.SelectionFlag.Rows)
|
|
w.edit.use_for_selected()
|
|
assert all(fx.summary(w.edit.store.get(k)["fx"]) == "Band-pass EQ, Echo, Channels"
|
|
for k in w.edit.store.list)
|
|
rack.set_channels(5)
|
|
assert "L R C SL SR" in rack.items["channels"].text()
|
|
|
|
|
|
def test_edit_store_moves_data_of_the_first_versions(qapp, tmp_path):
|
|
"""Replacements saved by the first versions (a "take" field, the takes/ folder) are found under the new names."""
|
|
import json
|
|
from app.edit import EditStore
|
|
root = tmp_path / "edits"
|
|
(root / "takes").mkdir(parents=True)
|
|
key = "INT:B49CDF48:1"
|
|
audio.save_wav(str(root / "takes" / "INT_B49CDF48_1.wav"), *synth.tone(0.2, 22050, 300))
|
|
json.dump({key: {"take": "recording", "gain": 1.5}}, open(root / "index.json", "w")) # first version: no list
|
|
st = EditStore(str(root))
|
|
assert st.list == [key] and st.get(key)["source"] == "recording" and "take" not in st.get(key)
|
|
assert st.has_replacement(key) and st.load_replacement(key) is not None and not (root / "takes").exists()
|