srctree

Andrew Kelley parent f70eee95 fdb1c800
implement pause and play button

README.md added: 166, removed: 13, total 153
@@ -56,14 +56,21 @@ This extension makes it easy to debug Zig WebAssembly code:
 
## Roadmap
 
* disable pause button when playing and vice versa
* playback UI element that shows waveform, duration, position, title
* prev, next, pause, play, stop
* prev, next, stop
* display the queue
* seeking
 
* bug: when the websocket disconnects and reconnects data gets duplicated
- it could clear the db and receive the full update again
- there could be a incrementing integer that describes the DB so the client
can avoid re-downloading if it has up-to-date data.
 
* implement queue item sort keys
* when scanning the file system, detect files that are in the database but have
been deleted from the file system
* implement scanning for true duration, loudness, and waveforms
* apply loudness information to playback
* use the thread pool when scanning the music directory
* example that transcodes a list of input files
 
client/main.zig added: 166, removed: 13, total 153
@@ -9,6 +9,7 @@ const js = struct {
extern "js" fn log(ptr: [*]const u8, len: usize) void;
extern "js" fn panic(ptr: [*]const u8, len: usize) noreturn;
extern "js" fn send(ptr: [*]const u8, len: usize) void;
extern "js" fn timestamp() i64;
};
 
pub const std_options: std.Options = .{
@@ -57,7 +58,10 @@ fn Slice(T: type) type {
 
var db = Db.empty;
var message_buffer: std.ArrayListAlignedUnmanaged(u8, @alignOf(u32)) = .{};
var server_timestamp: i64 = 0;
/// Nanoseconds.
var server_base_timestamp: i64 = 0;
/// Milliseconds.
var client_base_timestamp: i64 = 0;
 
/// Resizes the message buffer to be the correct length; returns the pointer to
/// the query string.
@@ -79,7 +83,8 @@ export fn message_end() void {
}
 
fn currentTimeMessage(msg_bytes: []u8) void {
server_timestamp = @bitCast(msg_bytes[1..][0..8].*);
client_base_timestamp = js.timestamp();
server_base_timestamp = @bitCast(msg_bytes[1..][0..8].*);
}
 
fn fullLibraryMessage(msg_bytes: []u8) error{OutOfMemory}!void {
@@ -168,6 +173,25 @@ export fn enqueueAlbum(index: Db.Album.Index) void {
send(std.mem.asBytes(&message));
}
 
export fn play() void {
const message: protocol.Command = .unpause;
send(std.mem.asBytes(&message));
}
 
export fn pause() void {
const message: protocol.Command = .pause;
send(std.mem.asBytes(&message));
}
 
export fn stop() void {
const message: protocol.Seek = .{
.command = .seek_pause,
.item = currentPlayQueueItemId() orelse return,
.position = Db.Duration.zero,
};
send(std.mem.asBytes(&message));
}
 
export fn queue_len() usize {
return db.queue_items.entries.len;
}
@@ -177,9 +201,40 @@ export fn queue_item_file(index: u32) Db.File.Index {
}
 
export fn file_title(index: Db.File.Index) String {
return String.init(db.stringToSlice(db.files.keys()[@intFromEnum(index)].title));
return String.init(db.stringToSlice(db.file(index).title));
}
 
fn send(bytes: []const u8) void {
js.send(bytes.ptr, bytes.len);
}
 
/// Nanoseconds passed since a server timestamp.
fn nsSince(server_timestamp: i64) i64 {
const ms_passed = js.timestamp() - client_base_timestamp;
const ns_passed = server_timestamp - server_base_timestamp;
return ns_passed + ms_passed * std.time.ns_per_ms;
}
 
fn currentPlayQueueItemId() ?Db.QueueItem.Id {
const started_item_id = db.state.started_item.unwrap() orelse return null;
if (!db.state.flags.playing) return started_item_id;
 
var seek_timestamp = nsSince(db.state.seek_timestamp);
const started_item_index = db.queueItemIndexById(started_item_id).?;
const list = db.queue_items.keys()[@intFromEnum(started_item_index)..];
 
for (list) |*item| {
const file = db.file(item.file);
const song_ns = @divTrunc(
(@as(i64, file.duration.sample_count) * std.time.ns_per_s),
@as(i64, file.duration.sample_rate),
);
if (seek_timestamp > song_ns) {
seek_timestamp -= song_ns;
continue;
}
return item.id;
}
 
return null;
}
 
server/main.zig added: 166, removed: 13, total 153
@@ -219,6 +219,14 @@ pub const Server = struct {
std.log.err("failed to enqueue album: {s}", .{@errorName(err)});
};
},
.pause => s.pause(true),
.unpause => s.pause(false),
.seek_pause => {
std.log.err("TODO implement seek_pause", .{});
},
.seek_play => {
std.log.err("TODO implement seek_play", .{});
},
_ => {
std.log.warn("unrecognized command from client: 0x{x}", .{msg[0]});
},
@@ -425,6 +433,16 @@ pub const Server = struct {
_ = opt_seek;
std.log.debug("TODO: implement broadcastPlayQueue", .{});
}
 
fn pause(s: *Server, paused: bool) void {
s.play_queue.mutex.lock();
defer s.play_queue.mutex.unlock();
 
const outstream = s.play_queue.outstream orelse return;
outstream.pause(paused) catch |err| {
std.log.err("failed to set pause state to {}: {s}", .{ paused, @errorName(err) });
};
}
};
 
fn scanDir(db: *Db, gpa: Allocator, db_dir: Db.Path.Index, it: *std.fs.Dir.Iterator) anyerror!void {
@@ -455,6 +473,7 @@ fn scanDir(db: *Db, gpa: Allocator, db_dir: Db.Path.Index, it: *std.fs.Dir.Itera
.track_number = undefined,
.composer = undefined,
.performer = undefined,
.duration = undefined,
});
if (file_gop.found_existing) continue;
db.files.lockPointers();
@@ -491,6 +510,7 @@ fn scanDir(db: *Db, gpa: Allocator, db_dir: Db.Path.Index, it: *std.fs.Dir.Itera
.track_number = metadata.track_number,
.composer = metadata.composer,
.performer = metadata.performer,
.duration = metadata.duration,
};
},
else => {
 
shared/Db.zig added: 166, removed: 13, total 153
@@ -170,6 +170,11 @@ pub const Album = extern struct {
pub const Duration = extern struct {
sample_count: u32,
sample_rate: u32,
 
pub const zero: Duration = .{
.sample_count = 0,
.sample_rate = 48000,
};
};
 
pub const File = extern struct {
@@ -181,6 +186,7 @@ pub const File = extern struct {
track_number: i16,
composer: OptionalString,
performer: OptionalString,
duration: Duration,
 
pub const Index = enum(u32) {
_,
@@ -210,6 +216,7 @@ pub const QueueItem = extern struct {
sort_key: SortKey,
};
 
/// A stable random number used as the key.
pub const Id = enum(u32) {
_,
 
@@ -222,6 +229,11 @@ pub const QueueItem = extern struct {
}
};
 
/// An index into `Db.queue_items`.
pub const Index = enum(u32) {
_,
};
 
pub const OptionalId = enum(u32) {
none = std.math.maxInt(u32),
_,
@@ -313,14 +325,29 @@ pub fn directory(db: *const Db, i: Path.Index) Path {
return db.directories.keys()[@intFromEnum(i)];
}
 
pub fn file(db: *Db, i: File.Index) *File {
return &db.files.keys()[@intFromEnum(i)];
}
 
pub fn queueItemIndexById(db: *const Db, id: QueueItem.Id) ?QueueItem.Index {
return if (db.queue_items.getIndex(.{
.id = id,
.flags = undefined,
.file = undefined,
})) |i|
@enumFromInt(i)
else
null;
}
 
pub fn fmtPath(db: *const Db, p: Path) std.fmt.Formatter(Path.format) {
return .{ .data = .{ .path = p, .db = db } };
}
 
pub fn musicDirPath(db: *const Db, file: *const File, buf: []u8) [:0]u8 {
pub fn musicDirPath(db: *const Db, f: *const File, buf: []u8) [:0]u8 {
return renderPath(db, .{
.parent = file.directory.toOptional(),
.basename = file.basename,
.parent = f.directory.toOptional(),
.basename = f.basename,
}, buf);
}
 
 
shared/protocol.zig added: 166, removed: 13, total 153
@@ -64,6 +64,10 @@ pub const PlayQueueHeader = extern struct {
 
pub const Command = enum(u8) {
enqueue_album,
pause,
unpause,
seek_pause,
seek_play,
_,
};
 
@@ -71,3 +75,9 @@ pub const EnqueueAlbum = extern struct {
command: Command = .enqueue_album,
album_index: Db.Album.Index align(1),
};
 
pub const Seek = extern struct {
command: Command,
item: Db.QueueItem.Id align(1),
position: Db.Duration align(1),
};
 
www/main.js added: 166, removed: 13, total 153
@@ -5,6 +5,11 @@
const domSectQueue = document.getElementById("sectQueue");
const domListLibrary = document.getElementById("listLibrary");
const domListQueue = document.getElementById("listQueue");
const domPrev = document.getElementById("prev");
const domStop = document.getElementById("stop");
const domPause = document.getElementById("pause");
const domPlay = document.getElementById("play");
const domNext = document.getElementById("next");
 
let ws = null;
let wasm_exports = null;
@@ -25,6 +30,9 @@
const bytes = new Uint8Array(wasm_exports.memory.buffer, ptr, len)
ws.send(bytes);
},
timestamp: function () {
return BigInt(new Date());
},
},
}).then(function(obj) {
wasm_exports = obj.instance.exports;
@@ -153,6 +161,11 @@
 
function initUi() {
domListLibrary.addEventListener('click', onClickLibrary, false);
domPrev.addEventListener('click', onClickPrev, false);
domStop.addEventListener('click', onClickStop, false);
domPause.addEventListener('click', onClickPause, false);
domPlay.addEventListener('click', onClickPlay, false);
domNext.addEventListener('click', onClickNext, false);
}
 
function onClickLibrary(ev) {
@@ -163,4 +176,25 @@
wasm_exports.enqueueAlbum(albumIndex);
}
}
 
function onClickPrev() {
wasm_exports.prev();
}
 
function onClickStop() {
wasm_exports.stop();
}
 
function onClickPause() {
wasm_exports.pause();
}
 
function onClickPlay() {
wasm_exports.play();
}
 
function onClickNext() {
wasm_exports.next();
}
 
})();