Skip to content

Commit 00edb46

Browse files
Merge branch 'master' into ddc-modernization-and-integration-14116118131799338522
2 parents e9697a6 + 3911220 commit 00edb46

70 files changed

Lines changed: 6845 additions & 0 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎PROJECT_ANALYSIS.md‎

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
# Project Analysis and Feature Roadmap
2+
3+
This document provides a comprehensive analysis of the `ddc-stepmania` project, its current state, and a detailed roadmap for future development.
4+
5+
## Current State of the Project
6+
7+
The project is currently in a foundational state. The core architecture is in place, the necessary data has been downloaded, and the codebase has been significantly refactored and modernized. However, the advanced features requested by the user are not yet implemented.
8+
9+
### Key Accomplishments:
10+
11+
* **Data Acquisition**: All 12 official DDR data packs have been successfully downloaded and extracted into the `data/raw/` directory.
12+
* **Project Structure**: The project has been organized into a proper Python package, `ddc_stepmania`, with a clear separation of concerns between data processing, model training, and inference.
13+
* **Code Modernization**: The codebase has been migrated from Python 2/TensorFlow 1.x to Python 3/TensorFlow 2.x (using the `compat.v1` module).
14+
* **Dependency Resolution**: Numerous bugs related to file paths, data loading, and missing libraries (`pydub`, `ffmpeg`) have been resolved.
15+
16+
### Existing Components:
17+
18+
* **`ddc_stepmania` package**:
19+
* `dataset`: Scripts for processing raw `.sm` files into JSON and then pickled Python objects.
20+
* `learn`: The core machine learning package, containing the `OnsetNet` and `SymNet` models, as well as the training scripts (`onset_train.py`, `sym_train.py`).
21+
* `infer`: Scripts and a server for using the trained models to generate new stepcharts.
22+
* **`ffr-difficulty-model`**: A pre-existing, integrated model for calculating the difficulty of a given stepchart.
23+
* **`scripts/`**: A collection of shell scripts that orchestrate the data processing and training pipelines.
24+
25+
## Feature Roadmap
26+
27+
The following is a detailed roadmap for the implementation of the remaining features, in order of priority.
28+
29+
### 1. Advanced Music Analysis
30+
31+
This is the most critical set of features, as it forms the foundation for generating high-quality stepcharts.
32+
33+
* **BPM and Tempo Change Detection**:
34+
* **Goal**: Accurately determine the BPM of a song, including any variations or tempo changes.
35+
* **Implementation**: Utilize the `librosa` library, specifically the `librosa.beat.beat_track` function, to perform robust beat tracking and tempo estimation.
36+
* **Integration**: The detected BPM and beat information will be stored in the processed data files and used to inform the `Chart` and `BeatCalc` classes.
37+
38+
* **First and Last Beat Identification**:
39+
* **Goal**: Pinpoint the exact timestamps of the first and last significant beats in the audio.
40+
* **Implementation**: This information will be derived directly from the output of the beat tracking algorithm.
41+
* **Integration**: This will be used to define the time window within which arrows can be placed, preventing them from appearing in silent sections of the song.
42+
43+
* **Automatic Music Sectioning**:
44+
* **Goal**: Segment a song into meaningful sections (e.g., intro, verse, chorus, bridge, outro).
45+
* **Implementation**: Use a structural segmentation algorithm, such as `librosa.segment.agglomerative`, to identify the major sections of the song.
46+
* **Integration**: The section boundaries will be used to create more musically interesting and varied stepcharts, with different patterns and densities for each section.
47+
48+
### 2. Refined Arrow Placement Logic
49+
50+
This set of features will focus on improving the quality and playability of the generated stepcharts.
51+
52+
* **Beat-Aligned Arrow Placement**:
53+
* **Goal**: Ensure that all generated arrows are perfectly aligned with the beat grid of the song.
54+
* **Implementation**: Modify the `OnsetNet` to predict onsets that are quantized to the beat grid. This will prevent the creation of off-beat or awkwardly timed arrows.
55+
* **Integration**: The `gen_labels.py` script will be updated to generate beat-aligned labels, and the `OnsetNet` will be retrained on this new data.
56+
57+
* **Pattern-Based Arrow Selection**:
58+
* **Goal**: Generate arrow patterns that are idiomatic to DDR and avoid random or unplayable sequences.
59+
* **Implementation**: The `SymNet` model will be enhanced to learn common DDR patterns (e.g., crossovers, jacks, streams). This could involve using a more sophisticated model architecture or incorporating a pattern library.
60+
* **Integration**: The `sym_train.py` script will be updated to train the new `SymNet` model, and the inference scripts will be modified to use it.
61+
62+
* **Playability Enforcement**:
63+
* **Goal**: Implement a post-processing step to clean up the generated charts and enforce playability rules.
64+
* **Implementation**: This will involve creating a set of rules to prevent things like double-steps (placing arrows on both feet at the same time), overly dense patterns, and other unplayable sequences.
65+
* **Integration**: A new script will be added to the `infer` package to perform this post-processing step.
66+
67+
### 3. Difficulty Generation and Rating
68+
69+
This set of features will focus on the automatic generation of charts for all difficulty levels and the assignment of accurate difficulty ratings.
70+
71+
* **Multi-Difficulty Generation**:
72+
* **Goal**: Automatically generate charts for all standard DDR difficulties (Beginner, Basic, Difficult, Expert, Challenge).
73+
* **Implementation**: This will likely involve training separate models for each difficulty level, or creating a single, conditional model that takes the desired difficulty as an input.
74+
* **Integration**: The training and inference pipelines will be updated to support the generation of multiple difficulty levels.
75+
76+
* **Difficulty Rating Integration**:
77+
* **Goal**: Use the `ffr-difficulty-model` to analyze the generated charts and assign them an accurate difficulty rating.
78+
* **Implementation**: The API of the `ffr-difficulty-model` will be used to analyze the generated `.sm` files.
79+
* **Integration**: The inference pipeline will be updated to include a final step where the difficulty rating is calculated and added to the generated chart's metadata.
80+
81+
### 4. Project Finalization
82+
83+
* **Packaging**: The entire project will be structured into a clean, distributable Python package, so that it can be easily installed and used by others.
84+
* **Documentation**: Comprehensive documentation will be created, including an updated `README.md`, docstrings for all key functions and classes, and a `CONTRIBUTING.md` file.
85+
* **Testing**: A suite of unit and integration tests will be created to ensure the correctness and robustness of the entire system.

‎convert_json_to_pickle.py‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import json
2+
import pickle
3+
import os
4+
import sys
5+
6+
def convert_json_to_pickle(file_list_path):
7+
"""
8+
Reads a file list (e.g., train.txt), loads each JSON file listed,
9+
and saves it as a pickle file. Creates a new file list with .pkl paths.
10+
"""
11+
if not os.path.exists(file_list_path):
12+
print(f"Error: File list not found at {file_list_path}")
13+
return
14+
15+
base_dir = os.path.dirname(file_list_path)
16+
file_list_name = os.path.basename(file_list_path)
17+
new_file_list_name = file_list_name.replace('.txt', '_pkl.txt')
18+
new_file_list_path = os.path.join(base_dir, new_file_list_name)
19+
20+
print(f"Converting files listed in {file_list_path}...")
21+
print(f"New file list will be saved to {new_file_list_path}")
22+
23+
with open(file_list_path, 'r') as f_in, open(new_file_list_path, 'w') as f_out:
24+
for json_path_line in f_in:
25+
json_path = json_path_line.strip()
26+
if not json_path:
27+
continue
28+
29+
pkl_path = os.path.splitext(json_path)[0] + '.pkl'
30+
31+
try:
32+
with open(json_path, 'r') as f_json:
33+
data = json.load(f_json)
34+
35+
with open(pkl_path, 'wb') as f_pkl:
36+
pickle.dump(data, f_pkl)
37+
38+
f_out.write(pkl_path + '\n')
39+
except Exception as e:
40+
print(f"Could not process {json_path}: {e}")
41+
42+
print("Conversion complete.")
43+
44+
if __name__ == "__main__":
45+
if len(sys.argv) != 2:
46+
print("Usage: python convert_json_to_pickle.py <path_to_file_list.txt>")
47+
sys.exit(1)
48+
49+
file_list = sys.argv[1]
50+
convert_json_to_pickle(file_list)

‎create_data_splits.py‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import random
2+
import os
3+
4+
def split_data(file_list_path, train_ratio=0.8, valid_ratio=0.1):
5+
"""
6+
Splits a list of file paths into training, validation, and test sets.
7+
"""
8+
if not os.path.exists(file_list_path):
9+
print(f"Error: File list not found at {file_list_path}")
10+
return
11+
12+
with open(file_list_path, 'r') as f:
13+
all_files = [line.strip() for line in f if line.strip()]
14+
15+
if not all_files:
16+
print("Error: The file list is empty.")
17+
return
18+
19+
random.shuffle(all_files)
20+
21+
num_files = len(all_files)
22+
num_train = int(num_files * train_ratio)
23+
num_valid = int(num_files * valid_ratio)
24+
25+
train_files = all_files[:num_train]
26+
valid_files = all_files[num_train:num_train + num_valid]
27+
test_files = all_files[num_train + num_valid:]
28+
29+
base_dir = os.path.dirname(file_list_path)
30+
31+
with open(os.path.join(base_dir, 'train_files.txt'), 'w') as f:
32+
f.write('\n'.join(train_files))
33+
34+
with open(os.path.join(base_dir, 'valid_files.txt'), 'w') as f:
35+
f.write('\n'.join(valid_files))
36+
37+
with open(os.path.join(base_dir, 'test_files.txt'), 'w') as f:
38+
f.write('\n'.join(test_files))
39+
40+
print(f"Data split complete:")
41+
print(f" Training set: {len(train_files)} files")
42+
print(f" Validation set: {len(valid_files)} files")
43+
print(f" Test set: {len(test_files)} files")
44+
45+
if __name__ == "__main__":
46+
split_data('all_sm_files.txt')

‎create_splits.py‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import os
2+
import random
3+
4+
def create_dataset_splits(data_dir, output_dir, train_split=0.8, valid_split=0.1):
5+
"""
6+
Reads .pkl files from a directory, shuffles them, and splits them into
7+
training, validation, and testing sets.
8+
"""
9+
if not os.path.exists(output_dir):
10+
os.makedirs(output_dir)
11+
12+
all_files = [os.path.join(data_dir, f) for f in os.listdir(data_dir) if f.endswith('.pkl')]
13+
random.shuffle(all_files)
14+
15+
train_end = int(len(all_files) * train_split)
16+
valid_end = int(len(all_files) * (train_split + valid_split))
17+
18+
train_files = all_files[:train_end]
19+
valid_files = all_files[train_end:valid_end]
20+
test_files = all_files[valid_end:]
21+
22+
with open(os.path.join(output_dir, 'train.txt'), 'w') as f:
23+
for item in train_files:
24+
f.write("%s\n" % item)
25+
26+
with open(os.path.join(output_dir, 'valid.txt'), 'w') as f:
27+
for item in valid_files:
28+
f.write("%s\n" % item)
29+
30+
with open(os.path.join(output_dir, 'test.txt'), 'w') as f:
31+
for item in test_files:
32+
f.write("%s\n" % item)
33+
34+
if __name__ == '__main__':
35+
create_dataset_splits('train.json', '.')

‎ddc_stepmania.egg-info/PKG-INFO‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Metadata-Version: 2.4
2+
Name: ddc_stepmania
3+
Version: 0.1
4+
License-File: LICENSE
5+
Dynamic: license-file

‎ddc_stepmania.egg-info/SOURCES.txt‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
LICENSE
2+
README.md
3+
setup.py
4+
ddc_stepmania.egg-info/PKG-INFO
5+
ddc_stepmania.egg-info/SOURCES.txt
6+
ddc_stepmania.egg-info/dependency_links.txt
7+
ddc_stepmania.egg-info/top_level.txt
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
if __name__ == '__main__':
2+
import argparse
3+
from collections import Counter, defaultdict
4+
import json
5+
import os
6+
from util import get_subdirs
7+
8+
parser = argparse.ArgumentParser()
9+
parser.add_argument('dataset_fps', type=str, nargs='+', help='List of dataset filepaths to analyze')
10+
parser.add_argument('--diff', type=str, help='If provided, only analyze charts of this difficulty')
11+
12+
parser.set_defaults(
13+
diff='')
14+
15+
args = parser.parse_args()
16+
17+
json_fps = []
18+
for dataset_fp in args.dataset_fps:
19+
with open(dataset_fp, 'r') as f:
20+
json_fps += f.read().splitlines()
21+
22+
chart_types = Counter()
23+
chart_diff_coarse = Counter()
24+
chart_feet = Counter()
25+
chart_coarse_to_stream = {}
26+
chart_freetexts = Counter()
27+
beat_phases = Counter()
28+
vocab = Counter()
29+
songs_time_annotated = 0.0
30+
charts_time_annotated = 0.0
31+
stream_total = 0.0
32+
feet_total = 0.0
33+
arrows_total = 0
34+
chart_coarse_to_superset = defaultdict(list)
35+
for json_fp in json_fps:
36+
with open(json_fp, 'r') as f:
37+
song_meta = json.loads(f.read())
38+
39+
max_time_annotated = -1.0
40+
coarse_to_beats = defaultdict(set)
41+
for chart_meta in song_meta['charts']:
42+
if args.diff and chart_meta['difficulty_coarse'] != args.diff:
43+
continue
44+
45+
coarse = chart_meta['difficulty_coarse']
46+
feet = chart_meta['difficulty_fine']
47+
feet_total += feet
48+
49+
chart_types[chart_meta['type']] += 1
50+
chart_diff_coarse[coarse] += 1
51+
chart_feet[feet] += 1
52+
chart_freetexts[chart_meta['desc_or_author']] += 1
53+
54+
num_arrows = 0
55+
for _, beat, time, arrow in chart_meta['notes']:
56+
beat_phase = beat - int(beat)
57+
beat_phase = int(beat_phase * 100.0) / 100.0
58+
beat_phases[beat_phase] += 1
59+
vocab[arrow] += 1
60+
if arrow != '0' * len(arrow):
61+
num_arrows += 1
62+
coarse_to_beats[coarse].add(beat)
63+
arrows_total += num_arrows
64+
65+
chart_time_annotated = chart_meta['notes'][-1][2] - chart_meta['notes'][0][2]
66+
if chart_time_annotated > max_time_annotated:
67+
max_time_annotated = chart_time_annotated
68+
charts_time_annotated += chart_time_annotated
69+
70+
stream = num_arrows / chart_time_annotated
71+
stream_total += stream
72+
if feet not in chart_coarse_to_stream:
73+
chart_coarse_to_stream[coarse] = []
74+
chart_coarse_to_stream[coarse].append(stream)
75+
76+
songs_time_annotated += max_time_annotated
77+
78+
coarses = ['Beginner', 'Easy', 'Medium', 'Hard', 'Challenge']
79+
for i, coarse in enumerate(coarses):
80+
for coarse_next in coarses:
81+
beats = coarse_to_beats[coarse]
82+
beats_next = coarse_to_beats[coarse_next]
83+
chart_coarse_to_superset[(coarse, coarse_next)].append(len(beats & beats_next) / float(len(beats)))
84+
85+
chart_coarse_to_stream = {k: sum(l) / len(l) for k, l in chart_coarse_to_stream.items()}
86+
chart_coarse_to_superset = {k: (reduce(lambda x, y: x + y, l) / len(l)) for k, l in chart_coarse_to_superset.items()}
87+
88+
nsongs = len(json_fps)
89+
ncharts = sum(chart_feet.values())
90+
print ','.join(args.dataset_fps)
91+
print 'Num songs: {}'.format(nsongs)
92+
print 'Total music annotated (s): {}'.format(songs_time_annotated)
93+
print 'Avg song length (s): {}'.format(songs_time_annotated / nsongs)
94+
95+
print 'Num charts: {}'.format(ncharts)
96+
print 'Avg num charts per song: {}'.format(float(ncharts) / nsongs)
97+
print 'Total chart time annotated (s): {}'.format(charts_time_annotated)
98+
print 'Avg chart length (s): {}'.format(charts_time_annotated / ncharts)
99+
print 'Avg chart length (steps): {}'.format(float(arrows_total) / ncharts)
100+
101+
print 'Chart types: {}'.format(chart_types)
102+
print 'Chart coarse difficulties: {}'.format(chart_diff_coarse)
103+
print 'Chart feet: {}'.format(chart_feet)
104+
print 'Chart coarse avg arrows per second: {}'.format(chart_coarse_to_stream)
105+
print 'Chart coarse avg superset: {}'.format(chart_coarse_to_superset)
106+
print 'Chart freetext fields: {}'.format(chart_freetexts)
107+
print 'Chart vocabulary (size={}): {}'.format(len(vocab), vocab)
108+
print 'Beat phases: {}'.format(beat_phases)
109+
110+
print 'Avg feet: {}'.format(feet_total / ncharts)
111+
print 'Avg arrows per second: {}'.format(stream_total / ncharts)

0 commit comments

Comments
 (0)