Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- --[[
- Tape Shuffler plays random tracks from a chest of tapes using a Computronics tape drive.
- 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.
- It needs a JSON library in order to operate.
- 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.
- You can use the program web2tape (https://pastebin.com/LW9RFpmY) to download a tape image to a tape.
- 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.
- To generate XTMF tape images, you can use generate-tape-image.py v2 (https://pastebin.com/EbHZq4uU).
- 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.
- Other encoders are currently in development.
- FOR DEVELOPERS:
- XTMF tapes contain the following:
- - Magic byte of 0x54
- - The length of the following JSON string as a big-endian 16-bit unsigned integer.
- - An (ASCII) string containing JSON (see "JSON Format" section). This may be referred to as the "metadata block".
- - 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.
- JSON Format:
- - 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.
- - 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).
- ]]
- local mode = ...
- local json = _G.json
- do
- local ok, x = pcall(require, "json")
- if ok then json = x end
- local ok, x = pcall(dofile, "json.lua")
- if ok then json = x end
- end
- local master_volume = settings.get "master_volume" or 1.0
- local tape = peripheral.find "tape_drive"
- local chest = peripheral.find(settings.get "tape_storage" or "minecraft:chest", function(name, object)
- object.network_name = name
- return true
- end)
- local monitor = peripheral.find "monitor"
- if monitor then monitor.setTextColor(colors.white) monitor.setBackgroundColor(colors.black) monitor.setTextScale(0.5) end
- local function seek_to_position(position)
- tape.seek(position - tape.getPosition())
- end
- -- Writing to tapes is easy. Reading is harder as they appear to be readable in 256-byte chunks at most.
- -- This reads until a NUL byte on the tape or until the max specified length is reached.
- -- This will probably cause problems eventually. Oh well.
- local function read_string(max_length)
- local remaining = max_length
- local data = ""
- while true do
- local block = tape.read(256)
- local nul_position = block:find "\0"
- if nul_position then -- block contains nul byte so end here
- block = block:sub(1, nul_position - 1)
- end
- block = block:sub(1, remaining)
- data = data .. block -- for some reason, only 256 chars at once can be read?
- remaining = remaining - #block
- if nul_position or remaining == 0 then return data end
- end
- end
- local function read_metadata_block()
- seek_to_position(0)
- -- The new metadata format is just "T" (0x54) then a big-endian 2-byte integer for metadata length then the existing JSON metadata.
- -- Someone suggested this to me and it seems much better than a fixed-length metadata block.
- -- (the T is only there to distinguish it from existing fixed-length-meta tapes)
- -- This does mean that the start/end positions for each track now need to be added to the metadata block (for new-format tapes).
- local first_byte = tape.read()
- local raw_json, meta_length = nil
- if first_byte == 0x54 then
- -- read bytes of the metadata length
- local jlen_hi = tape.read()
- local jlen_lo = tape.read()
- meta_length = jlen_hi * 256 + jlen_lo
- raw_json = read_string(meta_length)
- else
- seek_to_position(0)
- -- Fallback old-format processing
- -- Read 8KiB at most - this is a reasonable balance between using too much space and not fitting enough data
- raw_json = read_string(8192)
- end
- 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.
- if err then error(err) end
- -- shift start and end of tracks if new-format metadata being used
- if meta_length then
- for _, track in pairs(decoded.tracks) do
- -- account for the 3-byte header as well as metadata block
- track.start = track.start + 3 + meta_length
- track["end"] = track["end"] + 3 + meta_length
- end
- end
- return decoded
- end
- local default_bytes_per_second = 6000
- local function play_track(trackinfo)
- seek_to_position(trackinfo.start)
- local vol = (trackinfo.volume or 1.0) * master_volume
- tape.setVolume(math.max(math.min(vol, 1), 0))
- local length = trackinfo["end"] - trackinfo.start
- local speed = math.min(math.max(trackinfo.speed or 1.0, 0.5), 1)
- local length_seconds = (length / default_bytes_per_second) / speed
- tape.setSpeed(speed)
- tape.play()
- sleep(math.min(length_seconds - 2, 1)) -- start more precise end-reached checking a bit before approximate end
- repeat sleep(0.05)
- until tape.getPosition() >= trackinfo["end"]
- sleep(1) -- bodgey, but seems to make it work better, who knows why
- tape.stop()
- end
- local function load_by_slot(slot)
- -- unload current tape
- tape.pushItems(chest.network_name, 1)
- tape.pullItems(chest.network_name, slot)
- end
- local function load_by_label(label)
- for slot in pairs(chest.list()) do
- local meta = chest.getItemMeta(slot)
- -- if label matches,
- if meta.media and meta.media.label == label then
- load_by_slot(slot)
- return true
- end
- end
- return false
- end
- local function track_name(trackinfo)
- local out = trackinfo.title
- if trackinfo.album then out = trackinfo.album .. " - " .. out end
- if trackinfo.artist then out = trackinfo.artist .. " - " .. out end
- return out
- end
- local function load_track(trackinfo)
- if tape.getLabel() ~= trackinfo.tape then load_by_label(trackinfo.tape) end
- end
- if monitor then term.redirect(monitor) end
- tape.stop()
- tape.pushItems(chest.network_name, 1)
- local tape_meta = {}
- for slot in pairs(chest.list()) do
- load_by_slot(slot)
- tape_meta[tape.getLabel()] = read_metadata_block()
- end
- local tracks = {}
- for tape_label, meta in pairs(tape_meta) do
- for _, track in pairs(meta.tracks) do
- track.tape = tape_label
- table.insert(tracks, track)
- end
- end
- if mode == "dump_tracks" then
- local f = fs.open("tracks", "w")
- for _, track in pairs(tracks) do
- f.writeLine(json.encode(track))
- end
- f.close()
- return
- end
- local tracks_played = 0
- local function get_weight(track)
- if track.last_played then
- local tracks_ago_played = tracks_played - track.last_played
- return math.min(400, math.pow(tracks_ago_played, 2))
- else
- return 400
- end
- end
- -- weighted random choice
- local function pick_next_track()
- local sum = 0
- for _, track in ipairs(tracks) do
- track.weight = get_weight(track)
- sum = sum + track.weight
- end
- local index = math.random() * sum
- for _, track in ipairs(tracks) do
- index = index - track.weight
- if index <= 0 then return track end
- end
- end
- print(("Found %d tracks"):format(#tracks))
- while true do
- local track = pick_next_track()
- print(os.date "!%X", track_name(track))
- parallel.waitForAny(function()
- load_track(track)
- play_track(track)
- end,
- function()
- while true do
- local _, char = os.pullEvent "char"
- if char == "s" then
- print "Skipping to next track"
- tape.stop()
- return
- end
- end
- end)
- track.last_played = tracks_played
- tracks_played = tracks_played + 1
- end
Add Comment
Please, Sign In to add comment