text: import glob
  import logging as smlog
  import os
  import traceback
  
  from smdataset.abstime import calc_note_beats_and_abs_times
  from smdataset.parse import parse_sm_txt
  
  _ATTR_REQUIRED = ['offset', 'bpms', 'notes']
  
  if __name__ == '__main__':
      import argparse
      from collections import OrderedDict
      import json
      json.encoder.FLOAT_REPR = lambda f: ('%.6f' % f)
      from util import ez_name, get_subdirs
  
      parser = argparse.ArgumentParser()
      parser.add_argument('packs_dir', type=str, help='Directory of packs (organized like Stepmania songs folder)')
      parser.add_argument('json_dir', type=str, help='Output JSON directory')
      parser.add_argument('--itg', dest='itg', action='store_true', help='If set, subtract 9ms from offset')
      parser.add_argument('--choose', dest='choose', action='store_true', help='If set, choose from list of packs')
  
      parser.set_defaults(
          itg=False,
          choose=False)
  
      args = parser.parse_args()
  
      pack_names = get_subdirs(args.packs_dir, args.choose)
      pack_dirs = [os.path.join(args.packs_dir, pack_name) for pack_name in pack_names]
      pack_sm_globs = [os.path.join(pack_dir, '*', '*.sm') for pack_dir in pack_dirs]
  
      if not os.path.isdir(args.json_dir):
          os.mkdir(args.json_dir)
  
      pack_eznames = set()
      for pack_name, pack_sm_glob in zip(pack_names, pack_sm_globs):
          pack_sm_fps = sorted(glob.glob(pack_sm_glob))
          pack_ezname = ez_name(pack_name)
          if pack_ezname in pack_eznames:
              raise ValueError('Pack name conflict: {}'.format(pack_ezname))
          pack_eznames.add(pack_ezname)
  
          if len(pack_sm_fps) > 0:
              pack_outdir = os.path.join(args.json_dir, pack_ezname)
              if not os.path.isdir(pack_outdir):
                  os.mkdir(pack_outdir)
  
          sm_eznames = set()
          for sm_fp in pack_sm_fps:
              sm_name = os.path.split(os.path.split(sm_fp)[0])[1]
              sm_ezname = ez_name(sm_name)
              if sm_ezname in sm_eznames:
                  raise ValueError('Song name conflict: {}'.format(sm_ezname))
              sm_eznames.add(sm_ezname)
  
              with open(sm_fp, 'r') as sm_f:
                  sm_txt = sm_f.read()
  
              # parse file
              try:
                  sm_attrs = parse_sm_txt(sm_txt)
              except ValueError as e:
                  smlog.error('{} in\n{}'.format(e, sm_fp))
                  continue
              except Exception as e:
                  smlog.critical('Unhandled parse exception {}'.format(traceback.format_exc()))
                  raise e
  
              # check required attrs
              try:
                  for attr_name in _ATTR_REQUIRED:
                      if attr_name not in sm_attrs:
                          raise ValueError('Missing required attribute {}'.format(attr_name))
              except ValueError as e:
                  smlog.error('{}'.format(e))
                  continue
  
              # handle missing music
              root = os.path.abspath(os.path.join(sm_fp, '..'))
              music_fp = os.path.join(root, sm_attrs.get('music', ''))
              if 'music' not in sm_attrs or not os.path.exists(music_fp):
                  music_names = []
                  sm_prefix = os.path.splitext(sm_name)[0]
  
                  # check directory files for reasonable substitutes
                  for filename in os.listdir(root):
                      prefix, ext = os.path.splitext(filename)
                      if ext.lower()[1:] in ['mp3', 'ogg']:
                          music_names.append(filename)
  
                  try:
                      # handle errors
                      if len(music_names) == 0:
                          raise ValueError('No music files found')
                      elif len(music_names) == 1:
                          sm_attrs['music'] = music_names[0]
                      else:
                          raise ValueError('Multiple music files {} found'.format(music_names))
                  except ValueError as e:
                      smlog.error('{}'.format(e))
                      continue
  
                  music_fp = os.path.join(root, sm_attrs['music'])
  
              bpms = sm_attrs['bpms']
              offset = sm_attrs['offset']
              if args.itg:
                  # Many charters add 9ms of delay to their stepfiles to account for ITG r21/r23 global delay
                  # see http://r21freak.com/phpbb3/viewtopic.php?f=38&t=12750
                  offset -= 0.009
              stops = sm_attrs.get('stops', [])
  
              out_json_fp = os.path.join(pack_outdir, '{}_{}.json'.format(pack_ezname, sm_ezname))
              out_json = OrderedDict([
                  ('sm_fp', os.path.abspath(sm_fp)),
                  ('music_fp', os.path.abspath(music_fp)),
                  ('pack', pack_name),
                  ('title', sm_attrs.get('title')),
                  ('artist', sm_attrs.get('artist')),
                  ('offset', offset),
                  ('bpms', bpms),
                  ('stops', stops),
                  ('charts', [])
              ])
  
              for idx, sm_notes in enumerate(sm_attrs['notes']):
                  note_beats_and_abs_times = calc_note_beats_and_abs_times(offset, bpms, stops, sm_notes[5])
                  notes = {
                      'type': sm_notes[0],
                      'desc_or_author': sm_notes[1],
                      'difficulty_coarse': sm_notes[2],
                      'difficulty_fine': sm_notes[3],
                      'notes': note_beats_and_abs_times,
                  }
                  out_json['charts'].append(notes)
  
              with open(out_json_fp, 'w') as out_f:
                  try:
                      out_f.write(json.dumps(out_json))
                  except UnicodeDecodeError:
                      smlog.error('Unicode error in {}'.format(sm_fp))
                      continue
  
              print('Parsed {} - {}: {} charts'.format(pack_name, sm_name, len(out_json['charts'])))
  