osmarks

Tape Shuffler

Dec 23rd, 2019
489
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 4.43 KB | None | 0 0
  1. --[[
  2. Tape Shuffler plays random tracks from a chest of tapes using a Computronics tape drive.
  3. To allow for advanced features like actually being able to select tracks at all, these tapes must contain "tape images" containing metadata (in the "Extensible Tape Metadata Format"/XTMF), instead of a raw DFPWM file.
  4. It needs a JSON library in order to operate.
  5. By default it'll use one preloaded into the global table, which some servers provide, but if this is not available or has problems you can save RXI's json.lua (https://raw.githubusercontent.com/rxi/json.lua/bee7ee3431133009a97257bde73da8a34e53c15c/json.lua) to `json.lua` and it will be loaded.
  6. You can use the program web2tape (https://pastebin.com/LW9RFpmY) to download a tape image to a tape.
  7. Note that ComputerCraft appears to impose a size limit on HTTP requests, and thus you may need to split big (>~12MB) files into chunks and download them using the `norestart` option with web2tape.
  8.  
  9. To generate XTMF tape images, you can use generate-tape-image.py v2 (https://pastebin.com/EbHZq4uU).
  10. It takes a directory of music files, and reads their metadata (tested with m4a and Opus files, should work with more than that as it uses ffmpeg) to provide metadata for the tape, instead of relying on file structure.
  11. Other encoders are currently in development.
  12.  
  13. FOR DEVELOPERS:
  14.  
  15. XTMF tapes contain the following:
  16. - Magic byte of 0x54
  17. - The length of the following JSON string as a big-endian 16-bit unsigned integer.
  18. - An (ASCII) string containing JSON (see "JSON Format" section). This may be referred to as the "metadata block".
  19. - Raw DFPWM (1a) audio at the locations specified in the JSON. Some implementations may add empty padding between each track, as control of playback is not precise.
  20.  
  21. JSON Format:
  22. - The top level is an object with one key, `tracks`, which is an array of track objects. Some implementations may add additional data to this.
  23. - A track object must contain, at minimum, `title`, `start` (number of bytes after the metadata at which its data begins) and `end` (number of bytes after the metadata at which its data *ends*). It can contain, additionally, `artist` and `album` (for music tracks), `speed` (0.5 to 2.0, controls playback speed) and `volume` (0 to 1, controls playback volume).
  24. ]]
  25.  
  26. local mode = ...
  27.  
  28. local json = _G.json
  29. do
  30.     local ok, x = pcall(require, "json")
  31.     if ok then json = x end
  32.     local ok, x = pcall(dofile, "json.lua")
  33.     if ok then json = x end
  34. end
  35. local master_volume = settings.get "master_volume" or 1.0
  36. local tape = peripheral.find "tape_drive"
  37. local chest = peripheral.find(settings.get "tape_storage" or "minecraft:chest", function(name, object)
  38.     object.network_name = name
  39.     return true
  40. end)
  41. local monitor = peripheral.find "monitor"
  42. if monitor then monitor.setTextColor(colors.white) monitor.setBackgroundColor(colors.black) monitor.setTextScale(0.5) end
  43.  
  44. local function seek_to_position(position)
  45.     tape.seek(position - tape.getPosition())
  46. end
  47.  
  48. -- Writing to tapes is easy. Reading is harder as they appear to be readable in 256-byte chunks at most.
  49. -- This reads until a NUL byte on the tape or until the max specified length is reached.
  50. -- This will probably cause problems eventually. Oh well.
  51. local function read_string(max_length)
  52.     local remaining = max_length
  53.     local data = ""
  54.     while true do
  55.         local block = tape.read(256)
  56.         local nul_position = block:find "\0"
  57.         if nul_position then -- block contains nul byte so end here
  58.             block = block:sub(1, nul_position - 1)
  59.         end
  60.         block = block:sub(1, remaining)
  61.         data = data .. block -- for some reason, only 256 chars at once can be read?
  62.         remaining = remaining - #block
  63.         if nul_position or remaining == 0 then return data end
  64.     end
  65. end
  66.  
  67. local function read_metadata_block()
  68.     seek_to_position(0)
  69.     -- The new metadata format is just "T" (0x54) then a big-endian 2-byte integer for metadata length then the existing JSON metadata.
  70.     -- Someone suggested this to me and it seems much better than a fixed-length metadata block.
  71.     -- (the T is only there to distinguish it from existing fixed-length-meta tapes)
  72.     -- This does mean that the start/end positions for each track now need to be added to the metadata block (for new-format tapes).
  73.     local first_byte = tape.read()
  74.     local raw_json, meta_length = nil
  75.     if first_byte == 0x54 then
  76.         -- read bytes of the metadata length
  77.         local jlen_hi = tape.read()
  78.         local jlen_lo = tape.read()
  79.         meta_length = jlen_hi * 256 + jlen_lo
  80.         raw_json = read_string(meta_length)
  81.     else
  82.         seek_to_position(0)
  83.         -- Fallback old-format processing
  84.         -- Read 8KiB at most - this is a reasonable balance between using too much space and not fitting enough data
  85.         raw_json = read_string(8192)
  86.     end
  87.     local decoded, err = json.decode(raw_json) -- JSON is a big format, but very extensible and available pretty much everywhere. It may be worth switching to CBOR if size somehow becomes a problem.
  88.     if err then error(err) end
  89.  
  90.     -- shift start and end of tracks if new-format metadata being used
  91.     if meta_length then
  92.         for _, track in pairs(decoded.tracks) do
  93.             -- account for the 3-byte header as well as metadata block
  94.             track.start = track.start + 3 + meta_length
  95.             track["end"] = track["end"] + 3 + meta_length
  96.         end
  97.     end
  98.  
  99.     return decoded
  100. end
  101.  
  102. local default_bytes_per_second = 6000
  103. local function play_track(trackinfo)
  104.     seek_to_position(trackinfo.start)
  105.     local vol = (trackinfo.volume or 1.0) * master_volume
  106.     tape.setVolume(math.max(math.min(vol, 1), 0))
  107.     local length = trackinfo["end"] - trackinfo.start
  108.     local speed = math.min(math.max(trackinfo.speed or 1.0, 0.5), 1)
  109.     local length_seconds = (length / default_bytes_per_second) / speed
  110.     tape.setSpeed(speed)
  111.     tape.play()
  112.     sleep(math.min(length_seconds - 2, 1)) -- start more precise end-reached checking a bit before approximate end
  113.     repeat sleep(0.05)
  114.     until tape.getPosition() >= trackinfo["end"]
  115.     sleep(1) -- bodgey, but seems to make it work better, who knows why
  116.     tape.stop()
  117. end
  118.  
  119. local function load_by_slot(slot)
  120.     -- unload current tape
  121.     tape.pushItems(chest.network_name, 1)
  122.     tape.pullItems(chest.network_name, slot)
  123. end
  124.  
  125. local function load_by_label(label)
  126.     for slot in pairs(chest.list()) do
  127.         local meta = chest.getItemMeta(slot)
  128.         -- if label matches,
  129.         if meta.media and meta.media.label == label then
  130.             load_by_slot(slot)
  131.             return true
  132.         end
  133.     end
  134.     return false
  135. end
  136.  
  137. local function track_name(trackinfo)
  138.     local out = trackinfo.title
  139.     if trackinfo.album then out = trackinfo.album .. " - " .. out end
  140.     if trackinfo.artist then out = trackinfo.artist .. " - " .. out end
  141.     return out
  142. end
  143.  
  144. local function load_track(trackinfo)
  145.     if tape.getLabel() ~= trackinfo.tape then load_by_label(trackinfo.tape) end
  146. end
  147.  
  148. if monitor then term.redirect(monitor) end
  149. tape.stop()
  150. tape.pushItems(chest.network_name, 1)
  151.  
  152. local tape_meta = {}
  153. for slot in pairs(chest.list()) do
  154.     load_by_slot(slot)
  155.     tape_meta[tape.getLabel()] = read_metadata_block()
  156. end
  157.  
  158. local tracks = {}
  159. for tape_label, meta in pairs(tape_meta) do
  160.     for _, track in pairs(meta.tracks) do
  161.         track.tape = tape_label
  162.         table.insert(tracks, track)
  163.     end
  164. end
  165.  
  166. if mode == "dump_tracks" then
  167.     local f = fs.open("tracks", "w")
  168.     for _, track in pairs(tracks) do
  169.         f.writeLine(json.encode(track))
  170.     end
  171.     f.close()
  172.     return
  173. end
  174.  
  175. local tracks_played = 0
  176.  
  177. local function get_weight(track)
  178.     if track.last_played then
  179.         local tracks_ago_played = tracks_played - track.last_played
  180.         return math.min(400, math.pow(tracks_ago_played, 2))
  181.     else
  182.         return 400
  183.     end
  184. end
  185.  
  186. -- weighted random choice
  187. local function pick_next_track()
  188.     local sum = 0
  189.     for _, track in ipairs(tracks) do
  190.         track.weight = get_weight(track)
  191.         sum = sum + track.weight
  192.     end
  193.     local index = math.random() * sum
  194.     for _, track in ipairs(tracks) do
  195.         index = index - track.weight
  196.         if index <= 0 then return track end
  197.     end
  198. end
  199.  
  200. print(("Found %d tracks"):format(#tracks))
  201.  
  202. while true do
  203.     local track = pick_next_track()
  204.     print(os.date "!%X", track_name(track))
  205.     parallel.waitForAny(function()
  206.         load_track(track)
  207.         play_track(track)
  208.     end,
  209.     function()
  210.         while true do
  211.             local _, char = os.pullEvent "char"
  212.             if char == "s" then
  213.                 print "Skipping to next track"
  214.                 tape.stop()
  215.                 return
  216.             end
  217.         end
  218.     end)
  219.     track.last_played = tracks_played
  220.     tracks_played = tracks_played + 1
  221. end
Add Comment
Please, Sign In to add comment