Better difficulties, no bias for bass
This commit is contained in:
157
stepmania-ddr/README.md
Normal file
157
stepmania-ddr/README.md
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
# Herramientas de StepMania DDR
|
||||||
|
|
||||||
|
Este repositorio contiene dos herramientas útiles para trabajar con fichreos de **StepMania DDR (Dance Dance Revolution)**:
|
||||||
|
|
||||||
|
## 📋 Tabla de Contenidos
|
||||||
|
|
||||||
|
1. [StepMania Simplifier](#stepmania-simplifier)
|
||||||
|
2. [StepMania SM Generator](#stepmania-sm-generator)
|
||||||
|
3. [Requisitos Generales](#requisitos-generales)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔨 StepMania Simplifier
|
||||||
|
|
||||||
|
### ¿Qué hace?
|
||||||
|
|
||||||
|
**stepmania_simplifier.py** es una herramienta con interfaz gráfica (GUI) que permite **simplificar ficheros sm de StepMania existentes**. Es perfecta para:
|
||||||
|
|
||||||
|
- Crear versiones más fáciles de un gráfico base
|
||||||
|
- Reducir la dificultad eliminando patrones complejos
|
||||||
|
- Generar nuevas dificultades automáticamente a partir de una existente
|
||||||
|
|
||||||
|
### Características
|
||||||
|
|
||||||
|
La herramienta permite:
|
||||||
|
|
||||||
|
- **Eliminar notas rápidas**: Manteniendo o no además un porcentaje de ellas.
|
||||||
|
|
||||||
|
- **Eliminar saltos**: Opción para eliminar notas simultáneas (jumps)
|
||||||
|
|
||||||
|
- **Simplificar patrones**: Convertiendo holds largos en notas normales
|
||||||
|
|
||||||
|
- **Crear nueva dificultad**: La dificultad simplificada se guarda como una nueva dificultad con un nombre personalizado
|
||||||
|
|
||||||
|
### Dependencias
|
||||||
|
|
||||||
|
- Tkinter, incluida en Python en casi todos los casos.
|
||||||
|
|
||||||
|
### Cómo usar
|
||||||
|
|
||||||
|
1. Abre la aplicación:
|
||||||
|
|
||||||
|
2. Selecciona un archivo `.sm` (click en "Examinar")
|
||||||
|
|
||||||
|
3. El programa analizará automáticamente el archivo y mostrará las dificultades disponibles
|
||||||
|
|
||||||
|
4. Elige la dificultad base desde el dropdown "Chart Base"
|
||||||
|
|
||||||
|
5. Configura las opciones de simplificación según tus necesidades
|
||||||
|
|
||||||
|
6. Define el nombre para la nueva dificultad
|
||||||
|
|
||||||
|
7. Click en "Generar Versión Simplificada"
|
||||||
|
|
||||||
|
8. El archivo se guardará con la nueva dificultad añadida
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎵 StepMania SM Generator
|
||||||
|
|
||||||
|
### ¿Qué hace?
|
||||||
|
|
||||||
|
**stepmania_sm_generator.py** es un **generador automático de ficheros de StepMania a partir de archivos de audio**. Analiza la música y genera automáticamente los steps (pasos) con dificultades variables.
|
||||||
|
|
||||||
|
### Características
|
||||||
|
|
||||||
|
- Detección automática de **BPM** (tempo)
|
||||||
|
- Detección de **beats** y downbeats
|
||||||
|
- Detección de **onsets** (cambios musicales)
|
||||||
|
- Análisis espectral para posicionamiento inteligente de flechas
|
||||||
|
- Generación de múltiples niveles de dificultad (Beginner → Challenge)
|
||||||
|
- Conversión automática de audio a MP3 y videos a MP4 sin audio
|
||||||
|
|
||||||
|
### Dependencias
|
||||||
|
|
||||||
|
#### Requeridas:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install librosa numpy soundfile
|
||||||
|
```
|
||||||
|
|
||||||
|
**⚠️ Dependencia Opcional madmom, recomendada**:
|
||||||
|
|
||||||
|
La detección de BPM es MUCHO más precisa con `madmom`. Sin madmom, el programa usa `librosa` que puede equivocarse con géneros sincopados (reggaeton, trap, etc.)
|
||||||
|
|
||||||
|
Para Python > 3.9, instala desde el repositorio de GitHub:
|
||||||
|
```bash
|
||||||
|
pip install git+https://github.com/CPJKU/madmom.git
|
||||||
|
```
|
||||||
|
|
||||||
|
Para Python 3.8 o anterior:
|
||||||
|
```bash
|
||||||
|
pip install madmom
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Sistema:
|
||||||
|
|
||||||
|
- **ffmpeg**: Necesario para decodificar audio y video
|
||||||
|
- Ubuntu/Debian: `sudo apt install ffmpeg`
|
||||||
|
- macOS: `brew install ffmpeg`
|
||||||
|
- Windows: Descarga desde https://ffmpeg.org/download.html
|
||||||
|
|
||||||
|
### Cómo usar
|
||||||
|
|
||||||
|
1. Selecciona un archivo de audio (MP3, WAV, OGG, FLAC, etc.)
|
||||||
|
|
||||||
|
2. Selecciona un archivo de vídeo (Opcional)
|
||||||
|
|
||||||
|
3. El programa:
|
||||||
|
- Analiza automáticamente el audio
|
||||||
|
- Detecta el BPM y beats
|
||||||
|
- Genera los steps para múltiples dificultades
|
||||||
|
- Convierte archivos de video si es necesario
|
||||||
|
- Genera el archivo `.sm` final
|
||||||
|
|
||||||
|
|
||||||
|
### 🎯 Notas Importantes sobre BPM
|
||||||
|
|
||||||
|
El **BPM correcto es fundamental** para la calidad del gráfico generado. Si el programa no lo reconoce correctamente:
|
||||||
|
|
||||||
|
#### ¿Por qué es importante el BPM?
|
||||||
|
|
||||||
|
- Un BPM incorrecto hace que los beats no sincronicen con la música
|
||||||
|
- Los steps quedarán desalineados
|
||||||
|
- El gráfico será injugable
|
||||||
|
|
||||||
|
#### ¿Qué hacer si el BPM es incorrecto?
|
||||||
|
|
||||||
|
1. **Verificar el BPM manualmente** usando herramientas web:
|
||||||
|
- [BPM Detector Online](https://www.online-convert.com/file-converter)
|
||||||
|
- [Spotify](https://open.spotify.com) - Ver detalles de la canción
|
||||||
|
- [Tunebat](https://tunebat.com) - Detecta BPM de canciones
|
||||||
|
|
||||||
|
2. **Editar el BPM manualmente**: Añádelo en la GUI en el apartado de forzar BPM
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
## 🐛 Solución de Problemas
|
||||||
|
|
||||||
|
### "ffmpeg not found"
|
||||||
|
- Linux: `sudo apt install ffmpeg`
|
||||||
|
- macOS: `brew install ffmpeg`
|
||||||
|
- Windows: Descarga e instala desde https://ffmpeg.org/
|
||||||
|
|
||||||
|
### BPM detectado incorrectamente
|
||||||
|
- Asegúrate de tener **madmom** instalado
|
||||||
|
- Verifica manualmente el BPM usando herramientas web
|
||||||
|
- Edita el BPM en la GUI antes de hacer el proceso.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Licencia
|
||||||
|
|
||||||
|
Estas herramientas fueron creadas para trabajar con ficheros de StepMania DDR (.sm).
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import ttk, filedialog, messagebox, scrolledtext
|
from tkinter import ttk, filedialog, messagebox, scrolledtext
|
||||||
import os
|
import os
|
||||||
|
|||||||
@@ -1,22 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
# -*- coding: utf-8 -*-
|
||||||
StepMania .sm File Generator
|
|
||||||
=============================
|
|
||||||
Analyzes audio files and generates StepMania DDR step charts (.sm)
|
|
||||||
with multiple difficulty levels (Beginner → Challenge).
|
|
||||||
|
|
||||||
Uses audio analysis for step-chart generation:
|
|
||||||
- BPM detection via tempo estimation (madmom neural net or librosa fallback)
|
|
||||||
- Beat & downbeat tracking via DBN / accent-pattern analysis
|
|
||||||
- Onset detection via spectral flux
|
|
||||||
- Spectral band analysis for musically-aware arrow placement
|
|
||||||
- Optional background video conversion to MP4 (H.264, no audio)
|
|
||||||
|
|
||||||
Requirements:
|
|
||||||
pip install librosa numpy soundfile
|
|
||||||
pip install madmom # optional but STRONGLY recommended for accuracy (Install using git madmom repo if Python > 3.9)
|
|
||||||
System: ffmpeg (for audio/video decoding + conversion)
|
|
||||||
"""
|
|
||||||
|
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import ttk, filedialog, messagebox
|
from tkinter import ttk, filedialog, messagebox
|
||||||
@@ -703,10 +686,42 @@ class StepChartGenerator:
|
|||||||
def _log(self, msg, pct=0):
|
def _log(self, msg, pct=0):
|
||||||
self._cb(msg, pct)
|
self._cb(msg, pct)
|
||||||
|
|
||||||
|
def _row_color(self, subdiv, r_idx):
|
||||||
|
"""Return the pulse color for a row index within a measure."""
|
||||||
|
if subdiv == 8:
|
||||||
|
return 'red' if r_idx % 2 == 0 else 'blue'
|
||||||
|
if subdiv == 16:
|
||||||
|
mod = r_idx % 4
|
||||||
|
if mod == 0:
|
||||||
|
return 'red'
|
||||||
|
if mod == 2:
|
||||||
|
return 'blue'
|
||||||
|
return 'yellow'
|
||||||
|
return 'red'
|
||||||
|
|
||||||
|
def _map_band_to_arrow(self, t, band, cfg):
|
||||||
|
"""Map a spectral band to an arrow index with bass rotation."""
|
||||||
|
if band != 0:
|
||||||
|
return band
|
||||||
|
bpm = max(self.az.bpm, 1.0)
|
||||||
|
measure_len = 4.0 * 60.0 / bpm
|
||||||
|
measure_idx = int(max(t, 0.0) / measure_len)
|
||||||
|
# Rotate bass across lanes to avoid left bias; slower on lower diffs.
|
||||||
|
level = cfg.get('level', 6)
|
||||||
|
if level <= 3:
|
||||||
|
measures_per_step = 4
|
||||||
|
elif level <= 6:
|
||||||
|
measures_per_step = 2
|
||||||
|
else:
|
||||||
|
measures_per_step = 1
|
||||||
|
step_idx = measure_idx // measures_per_step
|
||||||
|
bass_cycle = [0, 1, 2, 3]
|
||||||
|
return bass_cycle[step_idx % 4]
|
||||||
|
|
||||||
# -- arrow assignment --
|
# -- arrow assignment --
|
||||||
def _pick_arrow(self, t, prev, cfg):
|
def _pick_arrow(self, t, prev, cfg, color=None):
|
||||||
band = self.az.get_dominant_band(t)
|
band = self.az.get_dominant_band(t)
|
||||||
arrow = band
|
arrow = self._map_band_to_arrow(t, band, cfg)
|
||||||
|
|
||||||
# 30 % random variety
|
# 30 % random variety
|
||||||
if self.rng.random() < 0.30:
|
if self.rng.random() < 0.30:
|
||||||
@@ -730,7 +745,14 @@ class StepChartGenerator:
|
|||||||
row[arrow] = 1
|
row[arrow] = 1
|
||||||
|
|
||||||
# jumps
|
# jumps
|
||||||
if cfg['jump_prob'] and self.rng.random() < cfg['jump_prob']:
|
jump_prob = cfg['jump_prob']
|
||||||
|
if cfg['level'] == 6 and color:
|
||||||
|
if color == 'red':
|
||||||
|
jump_prob = min(0.35, jump_prob * 1.6)
|
||||||
|
elif color == 'blue':
|
||||||
|
jump_prob = jump_prob * 0.4
|
||||||
|
|
||||||
|
if jump_prob and self.rng.random() < jump_prob:
|
||||||
alt = [i for i in range(4) if i != arrow]
|
alt = [i for i in range(4) if i != arrow]
|
||||||
row[self.rng.choice(alt)] = 1
|
row[self.rng.choice(alt)] = 1
|
||||||
|
|
||||||
@@ -772,6 +794,50 @@ class StepChartGenerator:
|
|||||||
row[:] = [0, 1, 0, 0] # switch to Down
|
row[:] = [0, 1, 0, 0] # switch to Down
|
||||||
prev_arrow = [i for i in range(4) if row[i]][0] if any(row) else prev_arrow
|
prev_arrow = [i for i in range(4) if row[i]][0] if any(row) else prev_arrow
|
||||||
|
|
||||||
|
# ---------- Rule 2b: De-repeat red/blue runs (medium) ----------
|
||||||
|
if cfg['level'] >= 6 and subdiv in (8, 16):
|
||||||
|
step = 1 if subdiv == 8 else 2
|
||||||
|
last_gi = None
|
||||||
|
last_arrow = None
|
||||||
|
last_color = None
|
||||||
|
for m_idx, meas in enumerate(measures):
|
||||||
|
for r_idx, row in enumerate(meas):
|
||||||
|
if sum(row) != 1:
|
||||||
|
last_gi = None
|
||||||
|
last_arrow = None
|
||||||
|
last_color = None
|
||||||
|
continue
|
||||||
|
|
||||||
|
color = self._row_color(subdiv, r_idx)
|
||||||
|
if color not in ('red', 'blue'):
|
||||||
|
last_gi = None
|
||||||
|
last_arrow = None
|
||||||
|
last_color = None
|
||||||
|
continue
|
||||||
|
|
||||||
|
gi = m_idx * subdiv + r_idx
|
||||||
|
arrow = [i for i in range(4) if row[i]][0]
|
||||||
|
|
||||||
|
if (last_gi is not None
|
||||||
|
and gi == last_gi + step
|
||||||
|
and last_color is not None
|
||||||
|
and color != last_color
|
||||||
|
and arrow == last_arrow):
|
||||||
|
# Alternate arrows/sides to avoid repetitive red-blue runs
|
||||||
|
if arrow in self.LEFT_FOOT:
|
||||||
|
candidates = self.RIGHT_FOOT + [1, 2]
|
||||||
|
else:
|
||||||
|
candidates = self.LEFT_FOOT + [1, 2]
|
||||||
|
candidates = [a for a in candidates if a != arrow]
|
||||||
|
new_arrow = self.rng.choice(candidates)
|
||||||
|
row[:] = [0, 0, 0, 0]
|
||||||
|
row[new_arrow] = 1
|
||||||
|
arrow = new_arrow
|
||||||
|
|
||||||
|
last_gi = gi
|
||||||
|
last_arrow = arrow
|
||||||
|
last_color = color
|
||||||
|
|
||||||
# ---------- Rule 3: Add emphasis jumps on downbeats (med+ diffs) ----------
|
# ---------- Rule 3: Add emphasis jumps on downbeats (med+ diffs) ----------
|
||||||
if cfg['level'] >= 5:
|
if cfg['level'] >= 5:
|
||||||
for m_idx, meas in enumerate(measures):
|
for m_idx, meas in enumerate(measures):
|
||||||
@@ -828,6 +894,53 @@ class StepChartGenerator:
|
|||||||
else:
|
else:
|
||||||
last_jump_gi = gi
|
last_jump_gi = gi
|
||||||
|
|
||||||
|
# ---------- Rule 6: Hard diff yellow-note constraints ----------
|
||||||
|
if cfg['level'] == 8 and subdiv == 16:
|
||||||
|
# Limit to one yellow per measure
|
||||||
|
for m_idx, meas in enumerate(measures):
|
||||||
|
yellow_rows = []
|
||||||
|
for r_idx, row in enumerate(meas):
|
||||||
|
if sum(row) == 0:
|
||||||
|
continue
|
||||||
|
if self._row_color(subdiv, r_idx) == 'yellow':
|
||||||
|
t = offset + (m_idx * subdiv + r_idx) * spr
|
||||||
|
yellow_rows.append((r_idx, self.az.get_rms_at(t)))
|
||||||
|
|
||||||
|
if len(yellow_rows) > 1:
|
||||||
|
yellow_rows.sort(key=lambda x: x[1], reverse=True)
|
||||||
|
for r_idx, _ in yellow_rows[1:]:
|
||||||
|
meas[r_idx][:] = [0, 0, 0, 0]
|
||||||
|
|
||||||
|
# Yellow notes cannot be jumps
|
||||||
|
for meas in measures:
|
||||||
|
for r_idx, row in enumerate(meas):
|
||||||
|
if self._row_color(subdiv, r_idx) != 'yellow':
|
||||||
|
continue
|
||||||
|
if sum(row) >= 2:
|
||||||
|
arrows = [i for i in range(4) if row[i]]
|
||||||
|
keep = self.rng.choice(arrows)
|
||||||
|
row[:] = [0, 0, 0, 0]
|
||||||
|
row[keep] = 1
|
||||||
|
|
||||||
|
# Remove yellow notes adjacent to jumps
|
||||||
|
jump_map = []
|
||||||
|
for m_idx, meas in enumerate(measures):
|
||||||
|
for r_idx, row in enumerate(meas):
|
||||||
|
jump_map.append(sum(row) >= 2)
|
||||||
|
|
||||||
|
total_rows = len(jump_map)
|
||||||
|
for m_idx, meas in enumerate(measures):
|
||||||
|
for r_idx, row in enumerate(meas):
|
||||||
|
if sum(row) == 0:
|
||||||
|
continue
|
||||||
|
if self._row_color(subdiv, r_idx) != 'yellow':
|
||||||
|
continue
|
||||||
|
gi = m_idx * subdiv + r_idx
|
||||||
|
left_jump = gi > 0 and jump_map[gi - 1]
|
||||||
|
right_jump = gi + 1 < total_rows and jump_map[gi + 1]
|
||||||
|
if left_jump or right_jump:
|
||||||
|
row[:] = [0, 0, 0, 0]
|
||||||
|
|
||||||
return measures
|
return measures
|
||||||
|
|
||||||
def _smooth_run(self, flat, start, length):
|
def _smooth_run(self, flat, start, length):
|
||||||
@@ -876,6 +989,25 @@ class StepChartGenerator:
|
|||||||
if ri >= 0 and abs((ot - offset) / spr - ri) < 0.45:
|
if ri >= 0 and abs((ot - offset) / spr - ri) < 0.45:
|
||||||
note_grid.add(ri)
|
note_grid.add(ri)
|
||||||
|
|
||||||
|
# Medium: favor more red/blue pulses based on energy
|
||||||
|
if cfg['level'] == 6:
|
||||||
|
total_rows = n_meas * subdiv
|
||||||
|
for gi in range(total_rows):
|
||||||
|
if gi in note_grid:
|
||||||
|
continue
|
||||||
|
r_idx = gi % subdiv
|
||||||
|
color = self._row_color(subdiv, r_idx)
|
||||||
|
if color not in ('red', 'blue'):
|
||||||
|
continue
|
||||||
|
trow = offset + gi * spr
|
||||||
|
if trow < self.az.music_start or trow > self.az.duration:
|
||||||
|
continue
|
||||||
|
rms = self.az.get_rms_at(trow)
|
||||||
|
base = 0.18 if color == 'red' else 0.12
|
||||||
|
prob = base * min(1.0, rms / 0.6)
|
||||||
|
if self.rng.random() < prob:
|
||||||
|
note_grid.add(gi)
|
||||||
|
|
||||||
# density cap
|
# density cap
|
||||||
max_notes = int(self.az.duration * cfg['max_nps'])
|
max_notes = int(self.az.duration * cfg['max_nps'])
|
||||||
if len(note_grid) > max_notes:
|
if len(note_grid) > max_notes:
|
||||||
@@ -891,7 +1023,8 @@ class StepChartGenerator:
|
|||||||
gi = m * subdiv + r
|
gi = m * subdiv + r
|
||||||
trow = offset + gi * spr
|
trow = offset + gi * spr
|
||||||
if gi in note_grid and 0 <= trow <= self.az.duration:
|
if gi in note_grid and 0 <= trow <= self.az.duration:
|
||||||
row, arrow = self._pick_arrow(trow, prev, cfg)
|
color = self._row_color(subdiv, r)
|
||||||
|
row, arrow = self._pick_arrow(trow, prev, cfg, color=color)
|
||||||
mrows.append(row)
|
mrows.append(row)
|
||||||
prev.append(arrow)
|
prev.append(arrow)
|
||||||
prev = prev[-8:]
|
prev = prev[-8:]
|
||||||
@@ -1046,6 +1179,10 @@ class App:
|
|||||||
self.v_seed = tk.StringVar()
|
self.v_seed = tk.StringVar()
|
||||||
self.v_bpm = tk.StringVar() # manual BPM override
|
self.v_bpm = tk.StringVar() # manual BPM override
|
||||||
self.diff_vars: dict[str, tk.BooleanVar] = {}
|
self.diff_vars: dict[str, tk.BooleanVar] = {}
|
||||||
|
self.last_dir = os.getcwd()
|
||||||
|
self._last_in = ""
|
||||||
|
|
||||||
|
self.v_in.trace_add('write', self._on_in_change)
|
||||||
|
|
||||||
self._build()
|
self._build()
|
||||||
|
|
||||||
@@ -1118,22 +1255,52 @@ class App:
|
|||||||
|
|
||||||
# ---- callbacks ----
|
# ---- callbacks ----
|
||||||
def _browse_in(self):
|
def _browse_in(self):
|
||||||
p = filedialog.askopenfilename(title="Select Audio", filetypes=self.AUDIO_TYPES)
|
p = filedialog.askopenfilename(
|
||||||
|
title="Select Audio",
|
||||||
|
filetypes=self.AUDIO_TYPES,
|
||||||
|
initialdir=self.last_dir,
|
||||||
|
)
|
||||||
if p:
|
if p:
|
||||||
self.v_in.set(p)
|
self.v_in.set(p)
|
||||||
self.v_out.set(str(Path(p).with_suffix('.sm')))
|
self.v_out.set(str(Path(p).with_suffix('.sm')))
|
||||||
|
self._update_last_dir(p)
|
||||||
|
|
||||||
def _browse_out(self):
|
def _browse_out(self):
|
||||||
p = filedialog.asksaveasfilename(
|
p = filedialog.asksaveasfilename(
|
||||||
title="Save .sm", defaultextension='.sm',
|
title="Save .sm", defaultextension='.sm',
|
||||||
filetypes=[('StepMania', '*.sm'), ('All', '*.*')])
|
filetypes=[('StepMania', '*.sm'), ('All', '*.*')],
|
||||||
|
initialdir=self.last_dir)
|
||||||
if p:
|
if p:
|
||||||
self.v_out.set(p)
|
self.v_out.set(p)
|
||||||
|
self._update_last_dir(p)
|
||||||
|
|
||||||
def _browse_vid(self):
|
def _browse_vid(self):
|
||||||
p = filedialog.askopenfilename(title="Select Video", filetypes=self.VIDEO_TYPES)
|
p = filedialog.askopenfilename(
|
||||||
|
title="Select Video",
|
||||||
|
filetypes=self.VIDEO_TYPES,
|
||||||
|
initialdir=self.last_dir,
|
||||||
|
)
|
||||||
if p:
|
if p:
|
||||||
self.v_vid.set(p)
|
self.v_vid.set(p)
|
||||||
|
self._update_last_dir(p)
|
||||||
|
|
||||||
|
def _update_last_dir(self, path):
|
||||||
|
p = Path(path).expanduser()
|
||||||
|
try:
|
||||||
|
self.last_dir = str(p.resolve().parent)
|
||||||
|
except Exception:
|
||||||
|
self.last_dir = str(p.parent)
|
||||||
|
|
||||||
|
def _on_in_change(self, *_):
|
||||||
|
p = self.v_in.get().strip()
|
||||||
|
if not p:
|
||||||
|
return
|
||||||
|
self._update_last_dir(p)
|
||||||
|
prev_out = self.v_out.get().strip()
|
||||||
|
auto_out = str(Path(self._last_in).with_suffix('.sm')) if self._last_in else ""
|
||||||
|
if not prev_out or prev_out == auto_out:
|
||||||
|
self.v_out.set(str(Path(p).with_suffix('.sm')))
|
||||||
|
self._last_in = p
|
||||||
|
|
||||||
def _log(self, msg, pct=None):
|
def _log(self, msg, pct=None):
|
||||||
def _do():
|
def _do():
|
||||||
|
|||||||
Reference in New Issue
Block a user