--- /dev/null
+
+FFMPEG=/opt/ffmpeg/4.0
+LMDB=/home/notzed/src/openldap/libraries/liblmdb
+WARN=-Wno-deprecated-declarations -Wno-parentheses -Wno-unused-but-set-variable
+
+pkgs=ffmpeg lmdb blkid asound espeak
+
+#CFLAGS_ffmpeg=-I$(FFMPEG)/include
+#LDFLAGS_ffmpeg=-L$(FFMPEG)/lib -Wl,-rpath,$(FFMPEG)/lib
+LDLIBS_ffmpeg=-lavformat -lavutil -lavcodec -lswresample -lavfilter
+
+#CFLAGS_lmdb=-I$(LMDB)
+#LDFLAGS_lmdb=-L$(LMDB) -Wl,-rpath,$(LMDB)
+LDLIBS_lmdb=-llmdb
+
+LDLIBS_blkid=-lblkid
+LDLIBS_asound=-lasound
+
+#CFLAGS_espeak=-I/opt/espeak/include
+#LDFLAGS_espeak=-L/opt/espeak/lib -Wl,-rpath,/opt/espeak/lib
+LDLIBS_espeak=-lespeak-ng
+
+CFLAGS=-std=gnu99 -I../libeze
+CFLAGS+=-Wall $(WARN)
+CFLAGS+=-g -O0
+CFLAGS+=$(foreach x,$(pkgs),$(CFLAGS_$(x)))
+LDFLAGS=$(foreach x,$(pkgs),$(LDFLAGS_$(x)))
+LDLIBS=$(foreach x,$(pkgs),$(LDLIBS_$(x))) -lrt -lpthread ../libeze/libeze.a
+
+PROGS=disk-indexer disk-monitor audio-cmd music-player input-monitor
+
+all: $(PROGS)
+
+disk-monitor: disk-monitor.o dbindex.o notify.o
+disk-indexer: disk-indexer.o dbindex.o notify.o
+audio-cmd: audio-cmd.o notify.o dbindex.o
+music-player: music-player.o notify.o dbindex.o
+input-monitor: input-monitor.o notify.o dbindex.o
+
+clean:
+ rm $(PROGS)
+ rm *.o
--- /dev/null
+
+#include <string.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#include "notify.h"
+
+/*
+static const struct {
+ const char *cmd;
+ enum notify_action action;
+} commands[] = {
+ { "pause", NOTIFY_PLAYER_PAUSE },
+ { "play", NOTIFY_PLAYER_PAUSE },
+ { "seek", NOTIFY_PLAYER_SEEKPAUSE },
+ { "pause", NOTIFY_PLAYER_PAUSE },
+};
+*/
+
+int main(int argc, char **argv) {
+ if (argc > 1) {
+ char *cmd = argv[1];
+ notify_t player = notify_writer_new(NOTIFY_PLAYER);
+
+ if (strcmp(cmd, "pause") == 0)
+ notify_msg_send(player, NOTIFY_PLAY_PAUSE, 1, NULL);
+ else if (strcmp(cmd, "play") == 0)
+ notify_msg_send(player, NOTIFY_PLAY_PLAY, 0, NULL);
+ else if (strcmp(cmd, "seek") == 0 || strcmp(cmd, "skip") == 0) {
+ if (argc == 3) {
+ struct notify_play_seek msg;
+
+ msg.mode = strcmp(cmd, "skip") == 0;
+ msg.stamp = strtod(argv[2], NULL);
+
+ notify_msg_send(player, NOTIFY_PLAY_SEEK, 0, &msg);
+ }
+ } else if (strcmp(cmd, "next") == 0) {
+ notify_msg_send(player, NOTIFY_PLAY_NEXT, 0, NULL);
+ } else if (strcmp(cmd, "prev") == 0) {
+ notify_msg_send(player, NOTIFY_PLAY_PREV, 0, NULL);
+ } else if (strcmp(cmd, "quit") == 0) {
+ notify_msg_send(player, NOTIFY_QUIT, 0, NULL);
+ } else if (strcmp(cmd, "debug") == 0) {
+ if (argc == 3) {
+ struct notify_debug msg;
+
+ msg.func = atoi(argv[2]);
+
+ notify_msg_send(player, NOTIFY_DEBUG, 0, &msg);
+ }
+ }
+
+ notify_close(player);
+
+ }
+}
--- /dev/null
+#include <stddef.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdint.h>
+#include <stdio.h>
+
+#include <assert.h>
+
+#include <lmdb.h>
+
+#include "dbindex.h"
+#include "ez-blob.h"
+
+ez_blob_desc DBDISK_DESC[] = {
+ EZ_BLOB_START(dbdisk),
+ EZ_BLOB_STRING(dbdisk, 1, uuid),
+ EZ_BLOB_STRING(dbdisk, 2, label),
+ EZ_BLOB_STRING(dbdisk, 3, type),
+ EZ_BLOB_STRING(dbdisk, 4, mount),
+ EZ_BLOB_END(dbdisk)
+};
+
+ez_blob_desc DBFILE_DESC[] = {
+ EZ_BLOB_START(dbfile),
+ EZ_BLOB_INT32(dbfile, 1, diskid),
+ EZ_BLOB_INT64(dbfile, 2, size),
+ EZ_BLOB_INT64(dbfile, 3, mtime),
+ EZ_BLOB_INT64(dbfile, 4, duration),
+ EZ_BLOB_STRING(dbfile, 5, path),
+ EZ_BLOB_STRING(dbfile, 6, title),
+ EZ_BLOB_STRING(dbfile, 7, artist),
+ EZ_BLOB_END(dbfile)
+};
+
+struct dbindex {
+ int res; // last result
+
+ MDB_env *env;
+
+ MDB_dbi meta;
+
+ MDB_dbi disk;
+ MDB_dbi disk_by_uuid; // key is uuid UNIQUE
+
+ MDB_dbi file;
+ MDB_dbi file_by_path; // key is "diskid{hex}/path" UNIQUE
+ MDB_dbi file_by_disk; // key is diskid FOREIGN
+ MDB_dbi file_by_title; // key is title (maybe all lower case?)
+ MDB_dbi file_by_artist; // key is artist
+
+ // ? maybe it should be a playlist ?
+ MDB_dbi shuffle; // seq to file
+ MDB_dbi shuffle_by_file;// file to seq FOREIGN
+
+ // This only works for threads not processes
+ // single writer I guess.
+ volatile uint32_t diskid;
+ volatile uint32_t fileid;
+};
+
+static uint32_t disk_next_id(dbindex *db) {
+ return __atomic_fetch_add(&db->diskid, 1, __ATOMIC_SEQ_CST);
+}
+
+static uint32_t file_next_id(dbindex *db) {
+ return __atomic_fetch_add(&db->fileid, 1, __ATOMIC_SEQ_CST);
+}
+
+// Find the next primary key in the db
+static uint32_t find_next_id(MDB_txn *tx, MDB_dbi db) {
+ MDB_cursor *cursor;
+ MDB_val key = { 0 }, data = { 0 };
+ int r;
+ uint32_t id = 1;
+
+ printf("find last value of db\n");
+ mdb_cursor_open(tx, db, &cursor);
+ r = mdb_cursor_get(cursor, &key, &data, MDB_LAST);
+ if (r == 0) {
+ assert(key.mv_size == sizeof(id));
+ memcpy(&id, key.mv_data, sizeof(id));
+ printf("found, was %d\n", id);
+ id += 1;
+ } else {
+ printf("not found (%d), using %d\n", r, id);
+ }
+
+ mdb_cursor_close(cursor);
+
+ return id;
+}
+
+dbindex *dbindex_open(const char *path) {
+ dbindex *db = calloc(sizeof(*db), 1);
+ int res;
+ MDB_txn *tx;
+
+ res = mdb_env_create(&db->env);
+ if (res)
+ goto fail;
+ res = mdb_env_set_maxdbs(db->env, 16);
+ if (res)
+ goto fail;
+ res = mdb_env_set_mapsize(db->env, 1<<28); // 256MB
+ if (res)
+ goto fail;
+ res = mdb_env_open(db->env, path, 0, 0664);
+ if (res)
+ goto fail;
+
+ res = mdb_txn_begin(db->env, NULL, 0, &tx);
+ if (res)
+ goto fail;
+
+ // ??
+ res |= mdb_dbi_open(tx, "#meta", MDB_CREATE, &db->meta);
+
+ res |= mdb_dbi_open(tx, "disk", MDB_CREATE | MDB_INTEGERKEY, &db->disk);
+ res |= mdb_dbi_open(tx, "disk#uuid", MDB_CREATE, &db->disk_by_uuid);
+
+ res |= mdb_dbi_open(tx, "file", MDB_CREATE | MDB_INTEGERKEY, &db->file);
+ res |= mdb_dbi_open(tx, "file#path", MDB_CREATE, &db->file_by_path);
+ res |= mdb_dbi_open(tx, "file#disk", MDB_CREATE | MDB_INTEGERKEY | MDB_DUPSORT | MDB_DUPFIXED | MDB_INTEGERDUP , &db->file_by_disk);
+ res |= mdb_dbi_open(tx, "file#title", MDB_CREATE | MDB_DUPSORT | MDB_DUPFIXED | MDB_INTEGERDUP, &db->file_by_title);
+ res |= mdb_dbi_open(tx, "file#artist", MDB_CREATE | MDB_DUPSORT | MDB_DUPFIXED | MDB_INTEGERDUP, &db->file_by_artist);
+
+ res |= mdb_dbi_open(tx, "shuffle", MDB_CREATE | MDB_INTEGERKEY, &db->shuffle);
+ res |= mdb_dbi_open(tx, "shuffle#file", MDB_CREATE | MDB_INTEGERKEY, &db->shuffle_by_file);
+
+ db->diskid = find_next_id(tx, db->disk);
+ db->fileid = find_next_id(tx, db->file);
+
+ {
+ MDB_cursor *cursor;
+ MDB_val key = { 0 }, data = { 0 };
+ int r;
+
+ printf("Known disks:\n");
+ mdb_cursor_open(tx, db->disk, &cursor);
+ r = mdb_cursor_get(cursor, &key, &data, MDB_FIRST);
+ while (r == 0) {
+ dbdisk *p = ez_blob_decode(DBDISK_DESC, data.mv_data, data.mv_size);
+ printf("id=%d\n", p->id);
+ printf(" uuid=%s\n", p->uuid);
+ printf(" label=%s\n", p->label);
+ printf(" type=%s\n", p->type);
+ printf(" mount=%s\n", p->mount);
+ ez_blob_free(DBDISK_DESC, p);
+ r = mdb_cursor_get(cursor, &key, &data, MDB_NEXT);
+ }
+ mdb_cursor_close(cursor);
+ }
+
+ res |= mdb_txn_commit(tx);
+
+ if (res) {
+ printf("db setup fail: %s\n", mdb_strerror(res));
+ // shutdown
+ mdb_env_close(db->env);
+ db = NULL;
+ }
+
+ printf("dbindex open, disk.id=%d file.id=%d\n", db->diskid, db->fileid);
+
+ return db;
+ fail:
+ printf("db setup `%s' fail: %s\n", path, mdb_strerror(res));
+ // shutdown
+ if (db->env)
+ mdb_env_close(db->env);
+ free(db);
+ return NULL;
+}
+
+void dbindex_close(dbindex *db) {
+ if (db) {
+ mdb_env_close(db->env);
+ free(db);
+ }
+}
+
+MDB_txn *dbindex_begin(dbindex *db, dbtxn *txn, int readonly) {
+ MDB_txn *tx;
+ int flags = readonly ? MDB_RDONLY : 0;
+
+ mdb_txn_begin(db->env, txn, flags, &tx);
+
+ return tx;
+}
+
+void dbindex_commit(MDB_txn *tx) {
+ mdb_txn_commit(tx);
+}
+
+void dbindex_abort(MDB_txn *tx) {
+ mdb_txn_abort(tx);
+}
+
+// get by_path key
+static char *dbfile_path(dbfile *f) {
+ char *path = malloc(strlen(f->path) + 9);
+
+ sprintf(path, "%08x%s", f->diskid, f->path);
+
+ return path;
+}
+
+// retrive object from main db
+static void *primary_get_decode(MDB_txn *tx, dbindex *db, ez_blob_desc *desc, MDB_val *key, MDB_dbi primary) {
+ MDB_val data;
+
+ db->res = mdb_get(tx, primary, key, &data);
+
+ if (db->res == 0) {
+ void *p = ez_blob_decode(desc, data.mv_data, data.mv_size);
+
+ assert(desc[0].bd_type == EZ_BLOB_PK);
+ assert(key->mv_size == sizeof(int));
+
+ memcpy(p, key->mv_data, sizeof(int));
+ return p;
+ }
+
+ return NULL;
+}
+
+/**
+ * Retrieve and decode data based on unique secondayry key.
+ *
+ * @param secondary key to retrieve
+ * @param data holder
+ */
+static void *secondary_get_decode(MDB_txn *tx, dbindex *db, ez_blob_desc *desc, MDB_val *key, MDB_dbi primary, MDB_dbi secondary) {
+ MDB_val data;
+
+ db->res = mdb_get(tx, secondary, key, &data);
+
+ if (db->res == 0)
+ return primary_get_decode(tx, db, desc, &data, primary);
+
+ return NULL;
+}
+
+dbdisk *dbdisk_get(dbtxn *tx, dbindex *db, int diskid) {
+ MDB_val key = { .mv_data = &diskid, .mv_size = sizeof(diskid) };
+
+ return primary_get_decode(tx, db, DBDISK_DESC, &key, db->disk);
+}
+
+dbdisk *dbdisk_get_uuid(MDB_txn *tx, dbindex *db, const char *uuid) {
+ MDB_val key = {
+ .mv_data = (void *)uuid,
+ .mv_size = strlen(uuid)
+ };
+
+ return secondary_get_decode(tx, db, DBDISK_DESC, &key, db->disk, db->disk_by_uuid);
+}
+
+void dbdisk_free(dbdisk *f) {
+ ez_blob_free(DBDISK_DESC, f);
+}
+
+int dbdisk_add(MDB_txn *txn, dbindex *db, dbdisk *d) {
+ MDB_txn *tx;
+ MDB_val key, data;
+ int res;
+
+ mdb_txn_begin(db->env, txn, 0, &tx);
+
+ // Store disk
+ d->id = disk_next_id(db);
+ key.mv_data = &d->id;
+ key.mv_size = sizeof(d->id);
+
+ if (1) {
+ data.mv_size = ez_blob_size(DBDISK_DESC, d);
+ data.mv_data = NULL;
+ res = mdb_put(tx, db->disk, &key, &data, MDB_NOOVERWRITE | MDB_RESERVE);
+ if (res == 0)
+ ez_blob_encode_raw(DBDISK_DESC, d, data.mv_data, data.mv_size);
+ } else {
+ void *blob = ez_blob_encode(DBDISK_DESC, d, &data.mv_size);
+
+ data.mv_data = blob;
+ res = mdb_put(tx, db->disk, &key, &data, MDB_NOOVERWRITE);
+ free(blob);
+ }
+
+ if (res != 0) {
+ printf("db put disk fail: %s\n", mdb_strerror(res));
+ return res;
+ }
+
+ // Store secondary keys
+ data.mv_data = &d->id;
+ data.mv_size = sizeof(d->id);
+
+ // - by uuid
+ key.mv_data = d->uuid;
+ key.mv_size = strlen(d->uuid);
+
+ res = mdb_put(tx, db->disk_by_uuid, &key, &data, MDB_NOOVERWRITE);
+ if (res == 0) {
+ return mdb_txn_commit(tx);
+ } else if (res == MDB_KEYEXIST) {
+ fprintf(stderr, "UNIQUE: disk uuid exists\n");
+ mdb_txn_abort(tx);
+ return res;
+ } else {
+ printf("db put fail: %s\n", mdb_strerror(res));
+ return res;
+ }
+
+}
+
+dbfile *dbfile_get_path(MDB_txn *tx, dbindex *db, int diskid, const char *path) {
+ char name[strlen(path) + 9];
+ MDB_val key;
+
+ sprintf(name, "%08x", diskid);
+ strcpy(name+8, path);
+
+ key.mv_data = name;
+ key.mv_size = strlen(name);
+
+ return secondary_get_decode(tx, db, DBFILE_DESC, &key, db->file, db->file_by_path);
+}
+
+void dbfile_free(dbfile *f) {
+ ez_blob_free(DBFILE_DESC, f);
+}
+
+dbfile *dbfile_get(dbtxn *tx, dbindex *db, int fileid) {
+ MDB_val key = { .mv_data = &fileid, .mv_size = sizeof(fileid) };
+
+ return primary_get_decode(tx, db, DBFILE_DESC, &key, db->file);
+}
+
+int dbfile_del_id(dbtxn *tx, dbindex *db, int fileid) {
+ dbfile *f = dbfile_get(tx, db, fileid);
+
+ if (f) {
+ db->res = dbfile_del(tx, db, f);
+ dbfile_free(f);
+ }
+
+ return db->res;
+}
+
+int dbfile_del(dbtxn *txn, dbindex *db, dbfile *f) {
+ MDB_txn *tx;
+ MDB_val key, data;
+ int res;
+
+ mdb_txn_begin(db->env, txn, 0, &tx);
+
+ // Remove secondary keys / constraints
+ data.mv_data = &f->id;
+ data.mv_size = sizeof(f->id);
+
+ // - by disk+path (and unique constraint)
+ char *dpath = dbfile_path(f);
+
+ key.mv_data = dpath;
+ key.mv_size = strlen(dpath);
+ res = mdb_del(tx, db->file_by_path, &key, NULL);
+ free(dpath);
+ if (res)
+ goto fail;
+
+ // - by diskid
+ key.mv_data = &f->diskid;
+ key.mv_size = sizeof(f->diskid);
+ if (res = mdb_del(tx, db->file_by_disk, &key, &data))
+ goto fail;
+
+ // - by title
+ if (f->title) {
+ key.mv_data = f->title;
+ key.mv_size = strlen(f->title);
+ if (res = mdb_del(tx, db->file_by_title, &key, &data))
+ goto fail;
+ }
+
+ // - by artist
+ if (f->artist) {
+ key.mv_data = f->artist;
+ key.mv_size = strlen(f->artist);
+ if (res = mdb_del(tx, db->file_by_artist, &key, &data))
+ goto fail;
+ }
+
+ // Remove file
+ key.mv_data = &f->id;
+ key.mv_size = sizeof(f->id);
+ if (res = mdb_del(tx, db->file, &key, NULL))
+ goto fail;
+
+ mdb_txn_commit(tx);
+ return res;
+
+ fail:
+ mdb_txn_abort(tx);
+ return res;
+}
+
+int dbstrcmp(const char *a, const char *b) {
+ if (a == NULL) {
+ if (b == NULL)
+ return 0;
+ else
+ return -1;
+ } else if (b == NULL)
+ return 1;
+
+ return strcmp(a, b);
+}
+
+// update o with any changed values
+int dbfile_update(dbtxn *txn, dbindex *db, dbfile *o, dbfile *f) {
+ MDB_txn *tx;
+ MDB_val key, data;
+ int res;
+
+ mdb_txn_begin(db->env, txn, 0, &tx);
+
+ // Update secondary keys
+ data.mv_data = &f->id;
+ data.mv_size = sizeof(f->id);
+
+ // - path can't change
+ // - diskid can't change
+ // - artist
+ if (dbstrcmp(o->artist, f->artist)) {
+ if (o->artist) {
+ key.mv_data = o->artist;
+ key.mv_size = strlen(o->artist);
+ if (res = mdb_del(tx, db->file_by_artist, &key, &data))
+ goto fail;
+ }
+
+ if (f->artist) {
+ key.mv_data = f->artist;
+ key.mv_size = strlen(f->artist);
+ if (res = mdb_put(tx, db->file_by_artist, &key, &data, 0))
+ goto fail;
+ }
+ }
+ // - title
+ if (dbstrcmp(o->title, f->title)) {
+ if (o->title) {
+ key.mv_data = o->title;
+ key.mv_size = strlen(o->title);
+ if (res = mdb_del(tx, db->file_by_title, &key, &data))
+ goto fail;
+ }
+
+ if (f->title) {
+ key.mv_data = f->title;
+ key.mv_size = strlen(f->title);
+ if (res = mdb_put(tx, db->file_by_title, &key, &data, 0))
+ goto fail;
+ }
+ }
+
+ f->id = o->id;
+ key.mv_data = &f->id;
+ key.mv_size = sizeof(f->id);
+
+ data.mv_size = ez_blob_size(DBFILE_DESC, f);
+ if (res = mdb_put(tx, db->file, &key, &data, MDB_RESERVE))
+ goto fail;
+
+ ez_blob_encode_raw(DBFILE_DESC, f, data.mv_data, data.mv_size);
+
+ return mdb_txn_commit(tx);
+
+ fail:
+ mdb_txn_abort(tx);
+ return res;
+}
+
+int dbfile_add(MDB_txn *txn, dbindex *db, dbfile *f) {
+ MDB_txn *tx;
+ MDB_val key, data;
+ int res;
+
+ mdb_txn_begin(db->env, txn, 0, &tx);
+
+ // Check foreign constraints
+ key.mv_data = &f->diskid;
+ key.mv_size = sizeof(f->diskid);
+ res = mdb_get(tx, db->disk, &key, &data);
+ if (res == MDB_NOTFOUND) {
+ mdb_txn_abort(tx);
+ fprintf(stderr, "FOREIGN KEY: file with unknown disk\n");
+ return res;
+ }
+
+ // Store file
+ f->id = file_next_id(db);
+ key.mv_data = &f->id;
+ key.mv_size = sizeof(f->id);
+
+ if (1) {
+ data.mv_size = ez_blob_size(DBFILE_DESC, f);
+ res = mdb_put(tx, db->file, &key, &data, MDB_NOOVERWRITE | MDB_RESERVE);
+ if (res == 0)
+ ez_blob_encode_raw(DBFILE_DESC, f, data.mv_data, data.mv_size);
+ } else {
+ void *blob = ez_blob_encode(DBFILE_DESC, f, &data.mv_size);
+
+ data.mv_data = blob;
+ res = mdb_put(tx, db->file, &key, &data, MDB_NOOVERWRITE);
+ free(blob);
+ }
+
+ if (res != 0) {
+ printf("db put file fail: %s\n", mdb_strerror(res));
+ goto fail;
+ }
+
+ // Store secondary keys
+ data.mv_data = &f->id;
+ data.mv_size = sizeof(f->id);
+
+ // - by disk+path (and unique constraint)
+ char *dpath = dbfile_path(f);
+
+ key.mv_data = dpath;
+ key.mv_size = strlen(dpath);
+ res = mdb_put(tx, db->file_by_path, &key, &data, MDB_NOOVERWRITE);
+ free(dpath);
+ if (res) {
+ fprintf(stderr, "UNIQUE: path on this disk exists\n");
+ goto fail;
+ }
+
+ // - by diskid
+ key.mv_data = &f->diskid;
+ key.mv_size = sizeof(f->diskid);
+ if (res = mdb_put(tx, db->file_by_disk, &key, &data, 0))
+ goto fail;
+
+ // - by title
+ if (f->title) {
+ key.mv_data = f->title;
+ key.mv_size = strlen(f->title);
+ if (res = mdb_put(tx, db->file_by_title, &key, &data, 0))
+ goto fail;
+ }
+
+ // - by artist
+ if (f->artist) {
+ key.mv_data = f->artist;
+ key.mv_size = strlen(f->artist);
+ if (res = mdb_put(tx, db->file_by_artist, &key, &data, 0))
+ goto fail;
+ }
+
+ return mdb_txn_commit(tx);
+
+ fail:
+ mdb_txn_abort(tx);
+ return res;
+}
+
+
+// TODO: this can be made generic for other indices
+struct dbscan {
+ dbindex *db;
+ MDB_cursor *cursor;
+ MDB_val key, data;
+ int keyval;
+ int index;
+ int count;
+};
+
+dbscan *dbfile_scan_disk(dbtxn *tx, dbindex *db, int diskid) {
+ dbscan *scan = malloc(sizeof(*scan));
+ int res;
+
+ scan->db = db;
+ scan->cursor = NULL;
+
+ scan->keyval = diskid;
+ scan->key.mv_data = &scan->keyval;
+ scan->key.mv_size = sizeof(scan->keyval);
+
+ if ((res = mdb_cursor_open(tx, db->file_by_disk, &scan->cursor)))
+ goto fail;
+
+ if (diskid != -1)
+ res = mdb_cursor_get(scan->cursor, &scan->key, &scan->data, MDB_SET);
+ else
+ res = mdb_cursor_get(scan->cursor, &scan->key, &scan->data, MDB_FIRST);
+
+ if (res) {
+ if (res == MDB_NOTFOUND) {
+ scan->count = 0;
+ scan->index = 0;
+ return scan;
+ }
+ goto fail;
+ }
+
+ if ((res = mdb_cursor_get(scan->cursor, &scan->key, &scan->data, MDB_GET_MULTIPLE)))
+ goto fail;
+
+ scan->count = scan->data.mv_size / sizeof(int);
+ scan->index = 0;
+
+ return scan;
+
+ fail:
+ fprintf(stderr, "db scan open fail: %s\n", mdb_strerror(res));
+ dbfile_scan_close(scan);
+ return NULL;
+}
+
+uint32_t dbfile_scan_next(dbscan *scan) {
+ int res = 0;
+
+ while (scan->count > 0) {
+ if (scan->index < scan->count)
+ return ((int *)scan->data.mv_data)[scan->index++];
+
+ if (res = mdb_cursor_get(scan->cursor, &scan->key, &scan->data, MDB_NEXT_MULTIPLE)) {
+ if (res == MDB_NOTFOUND && scan->keyval == -1) {
+ res = mdb_cursor_get(scan->cursor, &scan->key, &scan->data, MDB_NEXT);
+ if (res == 0)
+ res = mdb_cursor_get(scan->cursor, &scan->key, &scan->data, MDB_GET_MULTIPLE);
+ }
+ if (res)
+ goto fail;
+ }
+
+ scan->count = scan->data.mv_size / sizeof(int);
+ scan->index = 0;
+ }
+
+ return ~0;
+ fail:
+ if (res != MDB_NOTFOUND)
+ fprintf(stderr, "db scan fail: %s\n", mdb_strerror(res));
+ return ~0;
+}
+
+void dbfile_scan_close(dbscan *scan) {
+ if (scan->cursor)
+ mdb_cursor_close(scan->cursor);
+ free(scan);
+}
+
+
+/**
+ * Create a newly shuffled playlist.
+ */
+void dbshuffle_init(dbindex *db) {
+ dbtxn *tx;
+ dbscan *scan;
+ uint32_t fid;
+ int fids_size = 4096;
+ int count = 0;
+ int res;
+ uint32_t *fids = malloc(sizeof(*fids) * fids_size);
+
+ // find all current fids
+ mdb_txn_begin(db->env, NULL, 0, &tx);
+ scan = dbfile_scan_disk(tx, db, -1);
+ while ((fid = dbfile_scan_next(scan)) != ~0) {
+ if (count >= fids_size) {
+ fids_size *= 2;
+ fids = realloc(fids, sizeof(*fids) * fids_size);
+ }
+ fids[count++] = fid;
+ }
+ printf("total %d\n", count);
+ dbfile_scan_close(scan);
+
+ // now write them randomly
+ mdb_drop(tx, db->shuffle, 0);
+ mdb_drop(tx, db->shuffle_by_file, 0);
+
+ for (int i=0;i<count;i++) {
+ int j = random() % (count-i);
+ uint32_t seq = i + 1;
+ MDB_val key, data;
+
+ fid = fids[i+j];
+ fids[i+j] = fids[i];
+
+ key.mv_size = sizeof(int);
+ key.mv_data = &seq;
+ data.mv_size = sizeof(int);
+ data.mv_data = &fid;
+
+ res = mdb_put(tx, db->shuffle, &key, &data, MDB_NOOVERWRITE);
+ res = mdb_put(tx, db->shuffle_by_file, &data, &key, MDB_NOOVERWRITE);
+ }
+ free(fids);
+
+ dbindex_commit(tx);
+}
+
+/*
+ Player support functions
+*/
+
+// A way to iterate through a lit of files, based on an index or something else
+struct dblist {
+};
+
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include <dirent.h>
+
+/**
+ * Check if the disk is mounted.
+ *
+ * This is not generally absolutely reliable but is in the context of
+ * disk-monitor managing the mounts.
+ *
+ * It can be used for quickly discarding files that can't be mounted.
+ */
+int dbdisk_mounted(dbdisk *disk) {
+#if 0
+ // Check the device of the entries
+ char parent[strlen(disk->mount)+1];
+ char *slash = strrchr(parent, '/');
+
+ if (slash) {
+ struct stat pst, mst;
+
+ *slash = 0;
+
+ // Check if it's already mounted
+ return (stat(disk->mount, &mst) == 0
+ && stat(parent, &pst) == 0
+ && mst.st_dev != pst.st_rdev);
+ }
+
+ return 0;
+#else
+ // See if the directory is empty
+ DIR *d = opendir(disk->mount);
+ int entries = 0;
+
+ if (d) {
+ struct dirent *de;
+
+ while (de = readdir(d)) {
+ if (strcmp(de->d_name, ".") == 0
+ || strcmp(de->d_name, "..") == 0)
+ continue;
+ entries++;
+ }
+ closedir(d);
+ }
+
+ return entries > 0;
+#endif
+}
+
+
+/**
+ * Find the next file ... in some order or some index.
+ *
+ * This is intended to be called occasionally and not to scan the db.
+ *
+ * Currently it goes by the path index.
+ *
+ * should it be cyclic?
+ *
+ * FIXME: ignore files without audio! (duration==0)
+ *
+ * @param f file, use NULL to start at the beginning.
+ */
+static int dbfile_iterate(dbindex *db, dbfile **fp, char **pathp, int first, int next) {
+ MDB_txn *tx;
+ MDB_val key, data;
+ MDB_cursor *cursor;
+ dbfile *file = NULL;
+ int res;
+
+ mdb_txn_begin(db->env, NULL, MDB_RDONLY, &tx);
+
+ if ((res = mdb_cursor_open(tx, db->file_by_path, &cursor)))
+ goto fail;
+
+ /*
+ Scan based on filename order
+ */
+ char *keyval = *fp ? dbfile_path(*fp) : NULL;
+ dbdisk *disk = *fp ? dbdisk_get(tx, db, (*fp)->diskid) : NULL;
+ int mounted = *fp ? dbdisk_mounted(disk) : 0;
+
+ dbfile_free(*fp);
+ *fp = NULL;
+ free(*pathp);
+ *pathp = NULL;
+
+ if (keyval) {
+ key.mv_data = keyval;
+ key.mv_size = strlen(keyval);
+
+ res = mdb_cursor_get(cursor, &key, &data, MDB_SET);
+ res = mdb_cursor_get(cursor, &key, &data, next);
+ } else {
+ res = mdb_cursor_get(cursor, &key, &data, first);
+ }
+
+ while (file == NULL && res == 0) {
+ file = primary_get_decode(tx, db, DBFILE_DESC, &data, db->file);
+ if (file) {
+ int keep;
+
+ if (disk == NULL || file->diskid != disk->id) {
+ dbdisk_free(disk);
+ disk = dbdisk_get(tx, db, file->diskid);
+ mounted = dbdisk_mounted(disk);
+ }
+ keep = mounted;
+ keep = keep && file->duration > 0;
+ if (keep) {
+ char path[strlen(disk->mount) + strlen(file->path) + 1];
+ struct stat st;
+
+ sprintf(path, "%s%s", disk->mount, file->path);
+
+ keep = lstat(path, &st) == 0 && S_ISREG(st.st_mode);
+ if (keep) {
+ *pathp = strdup(path);
+ *fp = file;
+ }
+ }
+
+ if (!keep) {
+ dbfile_free(file);
+ file = NULL;
+ }
+ }
+ if (file == NULL)
+ res = mdb_cursor_get(cursor, &key, &data, next);
+ }
+
+ free(keyval);
+ dbdisk_free(disk);
+
+ mdb_cursor_close(cursor);
+ mdb_txn_commit(tx);
+
+ return res;
+ fail:
+ mdb_txn_abort(tx);
+
+ return res;
+}
+
+/**
+ * Find the next file ... in some order or some index.
+ *
+ * This is intended to be called occasionally and not to scan the db.
+ *
+ * Currently it goes by the path index.
+ *
+ * should it be cyclic?
+ *
+ * @param f file, use NULL to start at the beginning.
+ */
+int dbfile_next(dbindex *db, dbfile **f, char **fpath) {
+ return dbfile_iterate(db, f, fpath, MDB_FIRST, MDB_NEXT);
+}
+
+int dbfile_prev(dbindex *db, dbfile **f, char **fpath) {
+ return dbfile_iterate(db, f, fpath, MDB_LAST, MDB_PREV);
+}
+
+// this is a bit different to the by-path shuffle, since the path is used again as output rather
+// than the iteration key
+static int dbfile_iterate_shuffle(dbindex *db, dbfile **fp, char **pathp, int first, int next) {
+ MDB_txn *tx;
+ MDB_val key, data;
+ MDB_cursor *cursor;
+ dbfile *file = NULL;
+ int res;
+
+ mdb_txn_begin(db->env, NULL, MDB_RDONLY, &tx);
+
+ if ((res = mdb_cursor_open(tx, db->shuffle, &cursor)))
+ //if ((res = mdb_cursor_open(tx, db->shuffle_by_file, &cursor)))
+ goto fail;
+#if 0
+ printf("scan shuffle\n");
+ res = mdb_cursor_get(cursor, &key, &data, first);
+ while (res == 0) {
+ printf(" seq %d val %d\n", *((int *)key.mv_data), *((int *)data.mv_data));
+ res = mdb_cursor_get(cursor, &key, &data, next);
+ }
+
+ return 0;
+#endif
+
+ /*
+ Scan based on shuffle order
+ */
+ int *keyval = *fp ? &((*fp)->id) : NULL;
+ dbdisk *disk = *fp ? dbdisk_get(tx, db, (*fp)->diskid) : NULL;
+ int mounted = *fp ? dbdisk_mounted(disk) : 0;
+
+ printf("shuffle next, fid=%d\n", keyval ? *keyval : -1);
+
+ if (keyval) {
+ data.mv_data = keyval;
+ data.mv_size = sizeof(*keyval);
+
+ res = mdb_get(tx, db->shuffle_by_file, &data, &key);
+ printf("get by file = %d, id=%d\n", res, *((int *)key.mv_data));
+ if (res == MDB_NOTFOUND) {
+ printf("Not found\n");
+
+ return -1;
+ }
+
+ res = mdb_cursor_get(cursor, &key, &data, MDB_SET);
+ printf(" got shuffle id=%d fid=%d\n", *((int *)key.mv_data), *(int *)data.mv_data);
+
+ res = mdb_cursor_get(cursor, &key, &data, next);
+ printf(" next shuffle id=%d fid=%d\n", *((int *)key.mv_data), *(int *)data.mv_data);
+ } else {
+ res = mdb_cursor_get(cursor, &key, &data, first);
+ }
+
+ dbfile_free(*fp);
+ *fp = NULL;
+ free(*pathp);
+ *pathp = NULL;
+
+ while (file == NULL && res == 0) {
+ file = primary_get_decode(tx, db, DBFILE_DESC, &data, db->file);
+ if (file) {
+ int keep;
+
+ printf("loaded: %d[%d] %d?\n", *(int *)data.mv_data, data.mv_size, file->id);
+
+ if (disk == NULL || file->diskid != disk->id) {
+ dbdisk_free(disk);
+ disk = dbdisk_get(tx, db, file->diskid);
+ mounted = dbdisk_mounted(disk);
+ }
+ keep = mounted;
+ keep = keep && file->duration > 0;
+ if (keep) {
+ char path[strlen(disk->mount) + strlen(file->path) + 1];
+ struct stat st;
+
+ sprintf(path, "%s%s", disk->mount, file->path);
+
+ keep = lstat(path, &st) == 0 && S_ISREG(st.st_mode);
+ if (keep) {
+ *pathp = strdup(path);
+ *fp = file;
+ }
+ }
+
+ if (!keep) {
+ dbfile_free(file);
+ file = NULL;
+ }
+ }
+ if (file == NULL)
+ res = mdb_cursor_get(cursor, &key, &data, next);
+ }
+
+ //free(keyval);
+ dbdisk_free(disk);
+
+ mdb_cursor_close(cursor);
+ mdb_txn_commit(tx);
+
+ printf("laoded fid=%d\n", file->id);
+
+ return res;
+ fail:
+ mdb_txn_abort(tx);
+
+ return res;
+}
+
+int dbfile_next_shuffle(dbindex *db, dbfile **f, char **fpath) {
+ return dbfile_iterate_shuffle(db, f, fpath, MDB_FIRST, MDB_NEXT);
+}
+
+int dbfile_prev_shuffle(dbindex *db, dbfile **f, char **fpath) {
+ return dbfile_iterate_shuffle(db, f, fpath, MDB_LAST, MDB_PREV);
+}
+
+/*
+ Scan based on secondary index.
+
+ key.mv_data = &f->diskid;
+ key.mv_size = sizeof(f->diskid);
+
+ data.mv_data = &f->id;
+ data.mv_size = sizeof(f->id);
+
+ //res = mdb_cursor_get(scan->cursor, &scan->key, &scan->data, MDB_GET_BOTH);
+
+*/
--- /dev/null
+
+#include "ez-blob.h"
+
+typedef struct dbdisk dbdisk;
+
+struct dbdisk {
+ int id;
+ char *uuid;
+ char *label;
+ char *type;
+ char *mount; // last mount point
+};
+
+typedef struct dbfile dbfile;
+
+struct dbfile {
+ int id;
+ int diskid; // disk it belongs to
+
+ uint64_t size; // st_size
+ uint64_t mtime; // st_mtime
+
+ uint64_t duration; // duration from ffmpeg (in uS)
+
+ char *path; // relative path from disk root (with leading /)
+
+ char *title; // music title
+ char *artist; // music artist
+};
+
+typedef struct dbindex dbindex;
+typedef struct MDB_txn dbtxn;
+typedef struct dbscan dbscan;
+
+dbindex *dbindex_open(const char *path);
+void dbindex_close(dbindex *db);
+
+dbtxn *dbindex_begin(dbindex *db, dbtxn *txn, int readonly);
+void dbindex_commit(dbtxn *tx);
+void dbindex_abort(dbtxn *tx);
+
+dbdisk *dbdisk_get_uuid(dbtxn *tx, dbindex *db, const char *uuid);
+void dbdisk_free(dbdisk *d);
+int dbdisk_add(dbtxn *txn, dbindex *db, dbdisk *d);
+
+dbfile *dbfile_get(dbtxn *tx, dbindex *db, int fileid);
+dbfile *dbfile_get_path(dbtxn *tx, dbindex *db, int diskid, const char *path);
+void dbfile_free(dbfile *f);
+
+int dbfile_del(dbtxn *txn, dbindex *db, dbfile *f);
+int dbfile_add(dbtxn *txn, dbindex *db, dbfile *f);
+int dbfile_update(dbtxn *txn, dbindex *db, dbfile *o, dbfile *f);
+
+dbscan *dbfile_scan_disk(dbtxn *tx, dbindex *db, int diskid);
+uint32_t dbfile_scan_next(dbscan *scan);
+void dbfile_scan_close(dbscan *scan);
+
+extern ez_blob_desc DBDISK_DESC[];
+extern ez_blob_desc DBFILE_DESC[];
+
+/* Player support */
+int dbfile_next(dbindex *db, dbfile **f, char **fpath);
+int dbfile_prev(dbindex *db, dbfile **f, char **fpath);
+
+void dbshuffle_init(dbindex *db);
+
+int dbfile_next_shuffle(dbindex *db, dbfile **f, char **fpath);
+int dbfile_prev_shuffle(dbindex *db, dbfile **f, char **fpath);
+
+#define MAIN_INDEX "/home/notzed/playerz.db"
--- /dev/null
+
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <sys/time.h>
+#include <unistd.h>
+#include <dirent.h>
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <libavformat/avformat.h>
+
+#include <regex.h>
+
+#include "ez-list.h"
+#include "ez-set.h"
+#include "ez-bitset.h"
+
+#include "dbindex.h"
+
+#include "notify.h"
+
+struct indexer {
+
+ char *root;
+
+ dbindex *db;
+ dbdisk *disk;
+
+ dbtxn *tx; // global transaction for insert 1 disk
+
+ int isnew;
+
+ ez_list queue;
+ struct dirent *entry;
+
+ ez_bitset *existing;
+
+ // files to care about
+ regex_t match;
+
+ // some stats
+ struct timeval start;
+ int added;
+ int removed;
+ int updated;
+ int unchanged;
+};
+
+
+/*
+
+ Take a filename and generate some indexing info.
+
+ */
+
+struct AVDictionary {
+ int count;
+ AVDictionaryEntry *elems;
+};
+
+const char *pz_dict_get(AVDictionary *dict, const char *key, const char *def) {
+ if (dict) {
+ for (int i=0;i<dict->count;i++)
+ if (!strcmp(key, dict->elems[i].key))
+ return dict->elems[i].value;
+ }
+ return def;
+}
+
+static dbfile *scan_info(AVFormatContext *ic) {
+ int audio = 0;
+
+ for (int i=0;!audio && i<ic->nb_streams;i++) {
+ AVStream *s = ic->streams[i];
+ AVCodecContext *c = s->codec;
+
+ if (c->codec_type == AVMEDIA_TYPE_AUDIO) {
+ //printf(" audio: %s %dHz x%d %ld b/s\n",
+ // avcodec_get_name(c->codec_id),
+ // c->sample_rate,
+ // c->channels,
+ // c->bit_rate);
+ audio = 1;
+ }
+ }
+
+ dbfile *f = NULL;
+
+ if (audio) {
+ f = malloc(sizeof(*f));
+
+ f->id = 0;
+ f->artist = strdup(pz_dict_get(ic->metadata, "artist", "Unknown"));
+ f->title = strdup(pz_dict_get(ic->metadata, "title", "Unknown"));
+
+ //printf(" title: %s\n", f->title);
+ //printf(" artist: %s\n", f->artist);
+
+ if (ic->duration != AV_NOPTS_VALUE) {
+ //int64_t s = ic->duration / AV_TIME_BASE;
+ //int64_t u = ic->duration % AV_TIME_BASE;
+
+ // AV_TIME_BASE = 1 000 000, in us
+ f->duration = ic->duration;
+
+ //printf(" duration: %02d:%02d:%02d.%03d\n",
+ // (int)(s / 60 / 60),
+ // (int)(s / 60 % 60),
+ // (int)(s % 60),
+ // (int)((u + 500) / 1000));
+ } else {
+ f->duration = 0;
+ }
+ }
+
+ return f;
+}
+
+static dbfile *scan_file(const char *path) {
+ AVFormatContext *ic = NULL;
+ int res;
+ dbfile *f;
+
+ ic = avformat_alloc_context();
+ res = avformat_open_input(&ic, path, NULL, NULL);
+ if (res != 0)
+ return NULL;
+
+ res = avformat_find_stream_info(ic, NULL);
+
+ //printf("%s\n", path);
+ f = scan_info(ic);
+ //printf("\n");
+ //av_dump_format(ic, 0, path, 0);
+
+ avformat_close_input(&ic);
+
+ return f;
+}
+
+
+
+
+
+
+
+
+struct dir_node {
+ ez_node ln;
+ char path[0];
+};
+
+static struct dir_node *dir_new(const char *path) {
+ struct dir_node *n = malloc(sizeof(*n) + strlen(path)+1);
+
+ strcpy(n->path, path);
+
+ return n;
+}
+
+static void dir_free(struct dir_node *d) {
+ free(d);
+}
+
+void indexer_destroy(struct indexer *ix);
+
+int indexer_init(struct indexer *ix, dbindex *db, const char *path, const char *uuid) {
+ int res;
+
+ memset(ix, 0, sizeof(*ix));
+
+ ez_list_init(&ix->queue);
+ ez_list_addtail(&ix->queue, dir_new(path));
+
+ // find out how big dirent needs to be
+ int name_max = pathconf(path, _PC_NAME_MAX);
+
+ name_max = name_max == -1 ? 256 : name_max;
+ ix->entry = malloc(sizeof(struct dirent) - sizeof(ix->entry->d_name) + name_max + 1);
+
+ ix->root = strdup(path);
+
+ res = regcomp(&ix->match, "\\.(mp3|avi|mpeg|mp2|mpg|mp4|mov)$", REG_ICASE | REG_NOSUB | REG_EXTENDED);
+ if (res != 0) {
+ perror("regcomp");
+ exit(1);
+ }
+
+ // Lookup this disk
+ ix->db = db;
+ ix->tx = dbindex_begin(db, NULL, 0);
+
+ ix->disk = dbdisk_get_uuid(ix->tx, db, uuid);
+ if (!ix->disk) {
+ dbdisk *disk;
+
+ disk = malloc(sizeof(*ix->disk));
+
+ disk->id = 0;
+ disk->uuid = strdup(uuid);
+ disk->label = strdup("unknown");
+ disk->type = strdup("jfs");
+ disk->mount = strdup(path);
+
+ res = dbdisk_add(ix->tx, db, disk);
+ if (res != 0)
+ goto fail;
+ printf("%8d : add new disk %s\n", disk->id, uuid);
+ ix->disk = disk;
+ } else {
+ int count = 0;
+ dbtxn *tx = dbindex_begin(db, NULL, 1);
+
+ if (tx) {
+ dbscan *scan = dbfile_scan_disk(tx, db, ix->disk->id);
+ uint32_t fid;
+
+ if (scan) {
+ ix->existing = ez_bitset_new();
+
+ while ((fid = dbfile_scan_next(scan)) != ~0) {
+ count++;
+ ez_bitset_set(ix->existing, fid, 1);
+ }
+ dbfile_scan_close(scan);
+ }
+ dbindex_commit(tx);
+ printf("bitset count %d actual count %d\n", ez_bitset_card(ix->existing), count);
+ }
+
+ printf("%8d : add old disk %s (%d existing files)\n", ix->disk->id, uuid, count);
+ }
+
+ // FIXME: error handling
+
+ return 0;
+
+ fail:
+ if (ix->tx)
+ dbindex_abort(ix->tx);
+
+ indexer_destroy(ix);
+ return res;
+}
+
+void indexer_destroy(struct indexer *ix) {
+ if (ix->existing)
+ ez_bitset_free(ix->existing);
+
+ dbdisk_free(ix->disk);
+
+ struct dir_node *scan;
+ while ((scan = ez_list_remhead(&ix->queue)))
+ dir_free(scan);
+
+ free(ix->entry);
+ free(ix->root);
+ regfree(&ix->match);
+}
+
+/*
+ * Indexer "update" mechanism
+ *
+ * just don't bother.
+ *
+
+ 0. must have per-disk db
+
+ 1. if the same disk is (re)inserted
+ 2. scan files, and copy any existing db entries to new db if the timestamps match
+ 3. otherwise create new entries
+ 4. delete old db
+
+need to scan twice?
+
+ pass 1: copy any existing entries, updating as required (retain ids)
+ pass 2: create any entries which didn't exist with new id's
+
+single pass:
+
+ nextid is based on highest existing id, just use that for new entries
+
+
+in-place:
+
+ create set of all existing ids in db (bitmask or something)
+ scan and update, remove any visited from set
+ remove all those left in the set
+
+ -> needs to fuck around with updating secondary indices too!
+
+ what about garbage accumulation?
+
+ */
+
+// Add or update file
+int indexer_add_file(struct indexer *ix, struct stat *st, const char *filepath, const char *diskpath) {
+ dbfile *o = NULL, *f;
+
+ // If already there, and unchanged, do nothing
+ if (ix->existing) {
+ o = dbfile_get_path(ix->tx, ix->db, ix->disk->id, diskpath);
+
+ if (o) {
+ ez_bitset_set(ix->existing, o->id, 0);
+ if (o->size == st->st_size && o->mtime == st->st_mtime) {
+ dbfile_free(o);
+ ix->unchanged += 1;
+ return 0;
+ }
+ }
+ }
+
+ // Get or update metadata
+ // If it isn't an audio file, store it anyway to avoid a re-scan later
+ f = scan_file(filepath);
+ if (!f) {
+ f = malloc(sizeof(*f));
+ memset(f, 0, sizeof(*f));
+ }
+
+ f->path = strdup(diskpath);
+ f->diskid = ix->disk->id;
+ f->size = st->st_size;
+ f->mtime = st->st_mtime;
+
+ if (o) {
+ dbfile_update(ix->tx, ix->db, o, f);
+ ix->updated += 1;
+ } else {
+ dbfile_add(ix->tx, ix->db, f);
+ ix->added += 1;
+ }
+
+ dbfile_free(f);
+ dbfile_free(o);
+
+ return 0;
+}
+
+static void indexer_info(struct indexer *ix) {
+ struct timeval now;
+ time_t secs;
+
+ gettimeofday(&now, NULL);
+ secs = now.tv_sec - ix->start.tv_sec;
+ printf(" %02zd:%02zd:%02zd add: %6d rem: %6d same: %6d\n",
+ secs / (60 * 60),
+ (secs / 60) % 60,
+ secs % 60,
+ ix->added, ix->removed, ix->unchanged);
+}
+
+// scan a disk
+int indexer_scan(struct indexer *ix) {
+ struct dir_node *scan = NULL;
+ int count = 0, res = 0;
+
+ gettimeofday(&ix->start, NULL);
+
+ while ((scan = ez_list_remhead(&ix->queue))) {
+ //printf("scan %s\n", scan->path);
+
+ DIR *d = opendir(scan->path);
+ if (d) {
+ struct dirent *ep;
+ int dots = 0;
+
+ while (readdir_r(d, ix->entry, &ep) == 0 && ep) {
+ if (strcmp(ep->d_name, ".") == 0
+ || strcmp(ep->d_name, "..") == 0) {
+ // Hack: remove a usb disk while scanning can cause this to loop
+ dots++;
+ if (dots > 10)
+ break;
+ continue;
+ }
+
+ //usleep(5000);
+
+ char name[strlen(ep->d_name) + strlen(scan->path) + 2];
+ struct stat st;
+
+ sprintf(name, "%s/%s", scan->path, ep->d_name);
+
+ //printf(" %s\n", name);
+
+ // FIXME: use fstatat?
+ if ((res = lstat(name, &st)) == 0) {
+ if (S_ISREG(st.st_mode)) {
+ if (regexec(&ix->match, ep->d_name, 0, NULL, 0) == 0) {
+ indexer_add_file(ix, &st, name, name + strlen(ix->root));
+ count++;
+ if ((count % 1000) == 0)
+ indexer_info(ix);
+ }
+ } else if (S_ISDIR(st.st_mode)) {
+ ez_list_addtail(&ix->queue, dir_new(name));
+ } else if (S_ISLNK(st.st_mode)) {
+ // softlinks are ignored for simplicity reasons
+ }
+ } else {
+ perror("lstat");
+ if (errno == EIO)
+ goto fail;
+ }
+ }
+ closedir(d);
+ } else {
+ printf("open '%s': %s\n", scan->path, strerror(errno));
+ if (errno == EIO)
+ goto fail;
+ if (errno == ENOENT)
+ goto fail;
+ }
+ dir_free(scan);
+ }
+
+ if (ix->existing) {
+ printf("stale count: %d\n", ez_bitset_card(ix->existing));
+ ez_bitset_scan scan = { 0 };
+ uint32_t bit;
+
+ for (bit = ez_bitset_scan_init(ix->existing, &scan); bit != ~0; bit = ez_bitset_scan_next(&scan)) {
+ dbfile *f = dbfile_get(ix->tx, ix->db, bit);
+
+ if (f) {
+ printf(" stale %s\n", f->path);
+
+ dbfile_del(ix->tx, ix->db, f);
+ ix->removed += 1;
+
+ dbfile_free(f);
+ } else {
+ printf(" ** db corrupt missing file: %d\n", bit);
+ }
+ }
+ }
+
+ dbindex_commit(ix->tx);
+
+ indexer_info(ix);
+
+ return count;
+
+ fail:
+ if (scan)
+ dir_free(scan);
+
+ dbindex_abort(ix->tx);
+
+ return -1;
+}
+
+static void indexer(void) {
+ dbindex *db = dbindex_open(MAIN_INDEX);
+ notify_t q;
+ int quit = 0;
+
+ if (!db)
+ exit(1);
+
+ q = notify_reader_new(NOTIFY_INDEXER);
+ while (!quit) {
+ dbdisk *disk;
+ unsigned int pri;
+ enum notify_action action;
+
+ printf("Indexer: idle.\n");
+
+ disk = notify_msg_receive(q, &action, &pri);
+
+ switch (action) {
+ case NOTIFY_DISK_ADD: {
+ struct indexer ix;
+ int res;
+
+ printf("Indexer: scanning '%s'.\n", disk->mount);
+
+ res = indexer_init(&ix, db, disk->mount, disk->uuid);
+ if (res == 0) {
+ res = indexer_scan(&ix);
+
+ if (res >= 0) {
+ printf("Scan complete:\n");
+ printf(" added: %d\n", ix.added);
+ printf(" removed: %d\n", ix.removed);
+ printf(" updated: %d\n", ix.updated);
+ printf(" unchanged: %d\n", ix.unchanged);
+ } else {
+ printf("Scan aborted\n");
+ }
+
+ indexer_destroy(&ix);
+ }
+ break;
+ }
+ case NOTIFY_DISK_REMOVE:
+ // don't really care on this, indexing should fail if it was busy
+ break;
+ case NOTIFY_SHUFFLE:
+ printf("Creating shuffled playlist\n");
+ dbshuffle_init(db);
+ printf("Shuffle complete.\n");
+ break;
+ case NOTIFY_QUIT:
+ quit = 1;
+ break;
+ default:
+ break;
+ }
+ notify_msg_free(action, disk);
+ }
+
+ printf("Indexer: quit.\n");
+
+ dbindex_close(db);
+}
+
+void check(void) {
+ dbindex *db = dbindex_open(MAIN_INDEX);
+
+ // Check indices
+ printf("Check file-by-diskid index\n");
+ dbtxn *tx = dbindex_begin(db, NULL, 1);
+ dbscan *scan = dbfile_scan_disk(tx, db, -1);
+ uint32_t fid;
+ int count =0;
+
+ while ((fid = dbfile_scan_next(scan)) != ~0) {
+ dbfile *f = dbfile_get(tx, db, fid);
+ if (f == NULL) {
+ printf(" %d missing\n", fid);
+ } else {
+ printf(" in %d\n", fid);
+ }
+ dbfile_free(f);
+ count++;
+ }
+ printf("total %d\n", count);
+ dbfile_scan_close(scan);
+
+ dbindex_close(db);
+}
+
+int main(int argc, char **argv) {
+ av_log_set_level(AV_LOG_ERROR);
+
+ //avcodec_register_all();
+ //av_register_all();
+ //avformat_network_init();
+
+ mkdir(MAIN_INDEX, 0700);
+
+ if (argc > 1) {
+ if (strcmp(argv[1], "check") == 0)
+ check();
+ else {
+ notify_t q = notify_writer_new(NOTIFY_INDEXER);
+
+ if (q) {
+ if (strcmp(argv[1], "quit") == 0) {
+ notify_msg_send(q, NOTIFY_QUIT, 0, 0);
+ } else if (strcmp(argv[1], "shuffle") == 0) {
+ notify_msg_send(q, NOTIFY_SHUFFLE, 0, 0);
+ } else if (strcmp(argv[1], "add") == 0 && argc == 4) {
+ dbdisk disk = {
+ .uuid = argv[2],
+ .label = "system",
+ .type = "system",
+ .mount = argv[3]
+ };
+ notify_msg_send(q, NOTIFY_DISK_ADD, 0, &disk);
+ }
+ notify_close(q);
+ }
+ }
+ } else
+ indexer();
+
+ return 0;
+
+
+ char *uuid = argc > 1 ? argv[1] : "some-disk";
+
+
+ dbindex *db = dbindex_open("/home/notzed/playerz.db");
+ int res;
+
+ if (!db) {
+ return 1;
+ }
+
+ if (0) {
+ dbtxn *tx = dbindex_begin(db, NULL, 1);
+ int diskid = 4;
+ dbscan *scan = dbfile_scan_disk(tx, db, diskid);
+ int count = 0;
+ uint32_t fid;
+
+ while ((fid = dbfile_scan_next(scan)) != ~0) {
+ count++;
+ }
+ printf(" %d files on disk %d\n", count, diskid);
+
+ dbindex_commit(tx);
+ dbindex_close(db);
+ return 0;
+ }
+
+ struct indexer ix;
+
+ res = indexer_init(&ix, db, "/data/hd4/Music", uuid);
+ if (res == 0) {
+
+ //for (int i=1;i<argc;i++)
+ // scan_file(argv[i]);
+
+
+ res = indexer_scan(&ix);
+
+ if (res == 0) {
+ printf("Scan complete:\n");
+ printf(" added: %d\n", ix.added);
+ printf(" removed: %d\n", ix.removed);
+ printf(" updated: %d\n", ix.updated);
+ printf(" unchanged: %d\n", ix.unchanged);
+ } else {
+ printf("Scan aborted\n");
+ }
+
+ indexer_destroy(&ix);
+ }
+
+ dbindex_close(db);
+
+ return 0;
+}
--- /dev/null
+
+/*
+ disk-monitor
+
+ detects drive changes
+ mounts/umounts disk
+ sends events to listeners
+
+
+
+
+FIXME:
+
+ make a setuid-root binary
+ run as unprivileged user
+
+
+ wrap in as-root {
+ ...
+ mount/unmount
+ }
+
+ can this be wrapped in as-root? {
+ ... it only needs disk group?
+
+ p = blkid_new_probe_from_filename(dev);
+ }
+
+setup {
+ uid_t uid, euid;
+
+ uid = getuid(); // unpriv user
+ euid = geteuid(); // priv user
+}
+
+drop privs {
+ seteuid(uid);
+}
+
+raise privs {
+ seteuid(euid);
+}
+
+check returns.
+
+
+
+also perhaps setresuid() since that isn't so obtuse.
+
+euid = 0
+uid = 1000 or whatever
+
+drop {
+ setresuid(uid, uid, 0);
+}
+raise {
+ setresuid(uid, euid, 0);
+}
+
+
+or use capabilitties
+
+CAP_SETUID - arbitrary setuid
+CAP_SYS_ADMIN - mount disks
+
+the disk can be read with the disk group - nope, thats a udevd thing
+
+the keyboard can be read with the input group.
+ - but udev sets up the usb thing
+ */
+
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+#include <linux/netlink.h>
+#include <unistd.h>
+#include <dirent.h>
+#include <fcntl.h>
+
+#include <blkid/blkid.h>
+#include <sys/mount.h>
+
+#include <string.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <ctype.h>
+
+#include "dbindex.h"
+#include "ez-set.h"
+#include "ez-list.h"
+#include "notify.h"
+
+struct monitor {
+ ez_set *mounts;
+
+ size_t mount_base_size;
+ char *mount_base;
+
+ notify_t indexer;
+};
+
+#define MOUNT_BASE "/run/pzdisk"
+
+struct mdisk {
+ ez_node en;
+
+ char *dev;
+
+ struct dbdisk disk;
+};
+
+static unsigned int mdisk_hash(const void *mp) {
+ const struct mdisk *m = mp;
+
+ return ez_hash_string(m->dev);
+}
+
+static int mdisk_equals(const void *ap, const void *bp) {
+ const struct mdisk *a = ap, *b = bp;
+
+ return strcmp(a->dev, b->dev) == 0;
+}
+
+static void mdisk_free(void *mp) {
+ struct mdisk *m = mp;
+
+ free(m->dev);
+
+ free(m->disk.uuid);
+ free(m->disk.label);
+ free(m->disk.type);
+ free(m->disk.mount);
+
+ free(m);
+}
+
+static void print_values(const char *dev, blkid_probe p) {
+ int n = blkid_probe_numof_values(p), i;
+
+ printf("blkid probe %s\n", dev);
+ for (i=0;i<n;i++) {
+ const char *name, *value;
+ size_t size;
+
+ blkid_probe_get_value(p, i, &name, &value, &size);
+ printf(" %s: ", name);
+ fwrite(value, size, 1, stdout);
+ printf("\n");
+ }
+}
+
+void partition_notify(struct monitor *m, enum notify_action action, struct mdisk *md) {
+ int res = notify_msg_send(m->indexer, action, 0, &md->disk);
+
+ if (res)
+ perror("notify indexer");
+}
+
+/*
+ */
+
+static void partition_add(struct monitor *m, const char *dev) {
+ blkid_probe p = NULL;
+ int res;
+
+ printf("Add partition: %s\n", dev);
+
+ p = blkid_new_probe_from_filename(dev);
+ if (!p) {
+ perror("probe new");
+ return;
+ }
+
+ blkid_probe_enable_partitions(p, 0);
+ blkid_probe_enable_superblocks(p, 1);
+
+ res = blkid_do_safeprobe(p);
+ if (res != 0)
+ goto fail;
+
+ print_values(dev, p);
+
+ int ptype, puuid, plabel;
+ const char *type, *uuid, *label;
+ size_t type_size, uuid_size, label_size;
+
+ ptype = (blkid_probe_lookup_value(p, "TYPE", &type, &type_size) == 0);
+ puuid = (blkid_probe_lookup_value(p, "UUID", &uuid, &uuid_size) == 0);
+ plabel = (blkid_probe_lookup_value(p, "LABEL", &label, &label_size) == 0);
+
+ if (ptype && puuid) {
+ char *mountp = malloc(m->mount_base_size + uuid_size + 2);
+
+ memcpy(mountp, m->mount_base, m->mount_base_size);
+ mountp[m->mount_base_size] = '/';
+ memcpy(mountp + m->mount_base_size+1, uuid, uuid_size);
+ mountp[m->mount_base_size+1+uuid_size] = 0;
+
+ struct stat dst, mst;
+
+ // Check if it's already mounted
+ if (stat(mountp, &mst) == 0
+ && stat(dev, &dst) == 0
+ && mst.st_dev == dst.st_rdev) {
+ printf(" remount %s at %s\n", dev, mountp);
+ res = 0;
+ } else {
+ printf(" mount %s at %s\n", dev, mountp);
+ res = mkdir(mountp, 0777);
+ perror("mkdir");
+ res = mount(dev, mountp, type, MS_NOEXEC | MS_RDONLY | MS_NOSUID, NULL);
+ perror("mount");
+ }
+
+ if (res == 0) {
+ struct mdisk *md = malloc(sizeof(*md));
+
+ memset(md, 0, sizeof(*md));
+
+ md->dev = strdup(dev);
+ md->disk.uuid = strdup(uuid);
+ md->disk.type = strdup(type);
+ md->disk.label = plabel ? strdup(label) : NULL;
+ md->disk.mount = mountp;
+
+ partition_notify(m, NOTIFY_DISK_ADD, md);
+
+ md = ez_set_put(m->mounts, md);
+ if (md) {
+ printf("mounted twice?\n");
+ mdisk_free(md);
+ }
+
+ } else {
+ free(mountp);
+ perror("mount");
+ }
+ } else {
+ printf(" nomount %s unknown partition type\n", dev);
+ }
+
+ fail:
+ blkid_free_probe(p);
+}
+
+static void partition_remove(struct monitor *m, const char *dev) {
+ struct mdisk mde = {
+ .dev = (char *)dev
+ };
+ struct mdisk *md;
+ int res;
+
+ printf("Remove partition: %s\n", dev);
+
+ md = ez_set_remove(m->mounts, &mde);
+ if (md) {
+ partition_notify(m, NOTIFY_DISK_REMOVE, md);
+
+ res = umount2(md->disk.mount, MNT_DETACH);
+
+ if (res == 0) {
+ printf(" unmount %s @ %s\n", md->dev, md->disk.mount);
+ } else {
+ perror("umount2");
+ }
+
+ mdisk_free(md);
+ } else {
+ printf("Unknown partition removed!\n");
+ }
+}
+
+struct oldnode {
+ ez_node ln;
+
+ dev_t dev;
+ char *mount;
+};
+
+// scan to see if any partitions are already mounted
+static void partition_check(struct monitor *m) {
+ DIR *dir = opendir(m->mount_base);
+
+ if (dir) {
+ struct stat dstat;
+ struct stat estat;
+ struct dirent *e;
+ int dfd = dirfd(dir);
+ ez_list old = EZ_INIT_LIST(old);
+
+ fstat(dfd, &dstat);
+
+ while (e = readdir(dir)) {
+ if (fstatat(dfd, e->d_name, &estat, AT_SYMLINK_NOFOLLOW) == 0
+ && S_ISDIR(estat.st_mode)
+ && dstat.st_dev != estat.st_dev) {
+ struct oldnode *onode = malloc(sizeof(*onode));
+
+ onode->mount = malloc(m->mount_base_size + strlen(e->d_name) + 2);
+ sprintf(onode->mount, "%s/%s", m->mount_base, e->d_name);
+ onode->dev = estat.st_dev;
+
+ ez_list_addtail(&old, onode);
+ }
+ }
+
+ closedir(dir);
+
+ if (!ez_list_empty(&old)){
+ dir = opendir("/dev");
+ dfd = dirfd(dir);
+ while (e = readdir(dir)) {
+ if (fstatat(dfd, e->d_name, &estat, AT_SYMLINK_NOFOLLOW) == 0) {
+ // check if starts with sd?
+ struct oldnode *w, *n;
+
+ for (w = ez_list_head(&old), n = ez_node_succ(w);n;w=n,n=ez_node_succ(n)) {
+ if (w->dev == estat.st_rdev) {
+ char devname[strlen("/dev/") + strlen(e->d_name) + 1];
+
+ sprintf(devname, "/dev/%s", e->d_name);
+
+ partition_add(m, devname);
+ ez_node_remove(w);
+ free(w->mount);
+ free(w);
+ }
+ }
+ }
+ }
+ {
+ struct oldnode *w, *n;
+ for (w = ez_list_head(&old), n = ez_node_succ(w);n;w=n,n=ez_node_succ(n)) {
+ printf("Old mount unknown device %s\n", w->mount);
+ ez_node_remove(w);
+ free(w->mount);
+ free(w);
+ }
+ }
+ }
+
+ }
+}
+
+/**
+ * Monitors the KOBJECT_UEVENT socket and checks for hot-plugging of drives.
+ *
+ * Drives are then mounted/unmounted as required.
+ *
+ * Notification events are sent to coincide with these operations.
+ */
+static void monitor(void) {
+ struct monitor *m = malloc(sizeof(*m));
+ int s = socket(AF_NETLINK, SOCK_DGRAM, NETLINK_KOBJECT_UEVENT);
+ struct sockaddr_nl addr = {
+ .nl_family = AF_NETLINK,
+ .nl_pid = getpid(),
+ .nl_groups = ~0
+ };
+ int res;
+ char data[8192];
+ struct msghdr msg;
+ struct iovec iov;
+
+ bind(s, (void *)&addr, sizeof(addr));
+
+ iov.iov_base = data;
+ iov.iov_len = 8192;
+
+ msg.msg_name = (void *)&(addr);
+ msg.msg_namelen = sizeof(addr);
+ msg.msg_iov = &iov;
+ msg.msg_iovlen = 1;
+
+ memset(data, 0, sizeof(data));
+
+ //
+ m->mount_base = strdup(MOUNT_BASE);
+ m->mount_base_size = strlen(MOUNT_BASE);
+ m->mounts = ez_set_new(mdisk_hash, mdisk_equals, mdisk_free);
+ res = mkdir(m->mount_base, 0777);
+ m->indexer = notify_writer_new(NOTIFY_INDEXER);
+
+ partition_check(m);
+
+ printf("Monitoring uevents\n");
+
+ while ((res = recvmsg(s, &msg, 0)) >= 0) {
+ // ignore snot from udevd
+ if (strcmp(data, "libudev") == 0)
+ continue;
+
+ char *x = data, *e = data+res;
+ char *action;
+ char *dev;
+ char *type;
+
+ while (x < e) {
+ //printf(" %s\n", x);
+ if (strncmp(x, "ACTION=", 7) == 0) {
+ action = x+7;
+ } else if (strncmp(x, "DEVNAME=", 8) == 0) {
+ dev = x+8;
+ } else if (strncmp(x, "DEVTYPE=", 8) == 0) {
+ type = x+8;
+ }
+
+ x += strlen(x) + 1;
+ }
+ //printf("\n");
+
+ if (dev && type && strcmp(type, "partition") == 0) {
+ char name[strlen(dev) + strlen("/dev/") + 1];
+
+ sprintf(name, "/dev/%s", dev);
+
+ if (strcmp(action, "add") == 0)
+ partition_add(m, name);
+ else if (strcmp(action, "remove") == 0)
+ partition_remove(m, name);
+ }
+ memset(data, 0, res);
+ }
+
+ // unmount everything?
+
+ //
+ notify_close(m->indexer);
+ free(m->mount_base);
+ ez_set_free(m->mounts);
+
+ close(s);
+}
+
+int main(int argc, char **argv) {
+ monitor();
+}
--- /dev/null
+/**
+ This monitors the keyboard.
+
+ Or in this case a mele air mouse.
+
+ Could run this in-memory but as a separate process it can handle
+ some other crap like usb hotplug easier.
+
+ */
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <poll.h>
+#include <unistd.h>
+
+#include <linux/input.h>
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <stdint.h>
+#include <string.h>
+
+#define INPUT_KB "/dev/input/by-id/usb-2.4G_Wireless_Receiver-event-kbd"
+#define INPUT_MOUSE "/dev/input/by-id/usb-2.4G_Wireless_Receiver-if01-event-mouse"
+
+#include "notify.h"
+
+struct monitor {
+ int keyfd;
+ int mousefd;
+
+ notify_t player;
+
+ struct input_event repeat;
+};
+
+/*
+
+stop x from reading the airmouse
+
+$ xinput --list
+ .. find the airmouse devices (keyboard and mouse)
+$ xinput set-int-prop XXX "Device Enabled" 8 0
+
+*/
+
+/*
+
+
+ /dev/input/by-id/usb-2.4G_Wireless_Receiver-event-kbd
+
+enter: 28 KEY_ENTER
+home: 102 KEY_HOME
+
+up: 103 KEY_UP
+left: 105 KEY_LEFT
+right: 106 KEY_RIGHT
+down: 108 KEY_DOWN
+
+
+square: 59 KEY_F1
+cross: 60 KEY_F2
+circle: 61 KEY_F3
+triangle: 62 KEY_F4
+
+keyboard: same
+
+/dev/input/by-id/usb-2.4G_Wireless_Receiver-if01-event-mouse
+
+mute: 113 KEY_MUTE
+"-": 114 KEY_VOLUMEDOWN
+"+": 115 KEY_VOLUMEUP
+
+subtitles: 240 KEY_UNKNOWN
+lips: 240 KEY_UNKNOWN
+
+// mouse buttons
+centre: 272 BTN_LEFT
+back: 273 BTN_RIGHT
+menu: 274 BTN_MIDDLE
+
+display out: 377 KEY_TV
+*/
+
+struct keymap {
+ enum notify_action action;
+ short code;
+ unsigned char flags;
+ unsigned char mode;
+};
+
+// flags
+#define GEN_REPEAT 1
+
+static const struct keymap map[] = {
+ { NOTIFY_PLAY_SEEK, KEY_HOME, 0, 0 }, // house button
+ { NOTIFY_PLAY_PREV, KEY_UP },
+ { NOTIFY_PLAY_SEEK, KEY_LEFT, 0, 1},
+ { NOTIFY_PLAY_SEEK, KEY_RIGHT, 0, 2 },
+ { NOTIFY_PLAY_NEXT, KEY_DOWN },
+
+ { NOTIFY_VOLUME_MUTE, KEY_MUTE },
+ { NOTIFY_VOLUME_DOWN, KEY_VOLUMEDOWN, GEN_REPEAT }, // - button
+ { NOTIFY_VOLUME_UP, KEY_VOLUMEUP, GEN_REPEAT }, // + button
+};
+
+static int cmp_key(const void *ap, const void *bp) {
+ int needle = *((uint16_t *)ap);
+ const struct keymap *hay = bp;
+
+ return needle - hay->code;
+}
+
+struct monitor *monitor_new(void) {
+ struct monitor *m = malloc(sizeof(*m));
+
+ m->keyfd = open(INPUT_KB, O_RDONLY);
+ if (m->keyfd == -1)
+ goto fail;
+ m->mousefd = open(INPUT_MOUSE, O_RDONLY);
+ if (m->mousefd == -1)
+ goto fail1;
+
+ m->player = notify_writer_new(NOTIFY_PLAYER);
+
+ return m;
+ fail1:
+ close(m->keyfd);
+ fail:
+ free(m);
+
+ return NULL;
+}
+
+void monitor_free(struct monitor *m) {
+ notify_close(m->player);
+ close(m->mousefd);
+ close(m->keyfd);
+ free(m);
+}
+
+static void monitor_event(struct monitor *m, struct input_event *ev) {
+ if (ev->type == EV_KEY) {
+ struct keymap *key = bsearch(&ev->code, map, sizeof(map)/sizeof(map[0]), sizeof(map[0]), cmp_key);
+
+ if (key) {
+ // Special keys
+ printf(" key %d action %d flags %d mode %d\n", key->code, key->action, key->flags, key->mode);
+ if (ev->value) {
+ if (key->action == NOTIFY_PLAY_SEEK) {
+ struct notify_play_seek msg;
+
+ switch (key->mode) {
+ case 0:
+ msg.mode = 0;
+ msg.stamp = 0;
+ break;
+ case 1:
+ msg.mode = 1;
+ msg.stamp = -15;
+ break;
+ case 2:
+ msg.mode = 1;
+ msg.stamp = +15;
+ break;
+ }
+ notify_msg_send(m->player, key->action, 0, &msg);
+ } else {
+ notify_msg_send(m->player, key->action, 0, NULL);
+ }
+
+ if (key->flags & GEN_REPEAT) {
+ printf("start repeat\n");
+ m->repeat = *ev;
+ }
+ } else {
+ if (key->flags & GEN_REPEAT) {
+ printf("stop repeat\n");
+ memset(&m->repeat, 0, sizeof(m->repeat));
+ }
+ }
+ } else if (ev->value) {
+ // All others, sort of
+ if (ev->code == BTN_MIDDLE)
+ ev->code = KEY_MENU;
+ else if (ev->code == KEY_UNKNOWN)
+ ev->code = KEY_HELP;
+ printf(" ev %d %04x %08x\n", ev->type, ev->code, ev->value);
+ struct notify_key msg = {.code = ev->code };
+ notify_msg_send(m->player, NOTIFY_KEY, 0, &msg);
+ }
+ }
+}
+
+/*
+ switch (ev.code) {
+ case KEY_ENTER: // enter button
+ case KEY_F1: // square
+ case KEY_F2: // cross
+ case KEY_F3: // circle
+ case KEY_F4: // triangle
+
+ case KEY_HOME: // house button
+
+ case KEY_UP: // face panel up
+ case KEY_DOWN: // face panel down
+ case KEY_LEFT: // face panel left
+ case KEY_RIGHT: // face panel right
+
+ case KEY_MUTE: // mute
+ case KEY_VOLUMEDOWN: // - button
+ case KEY_VOLUMEUP: // + button
+
+ case KEY_UNKNOWN: // both the lips and the subtitle button
+
+ case BTN_LEFT: // face panel centre
+ case BTN_RIGHT: // back button
+ case BTN_MIDDLE: // menu /sandwich button
+
+ case KEY_TV: // 'output' button
+ break;
+
+ }
+*/
+
+void monitor(struct monitor *m) {
+ struct pollfd polla[2];
+ struct input_event ev;
+
+ // NB: if the player isn't running this will block
+
+ polla[0].fd = m->keyfd;
+ polla[0].events = POLLIN;
+ polla[1].fd = m->mousefd;
+ polla[1].events = POLLIN;
+
+ while (1) {
+ printf("poll, timeout = %d\n", m->repeat.value != 0 ? 50 : -1);
+
+ int res = poll(polla, 2, m->repeat.value != 0 ? 50 : -1);
+ if (res > 0) {
+ for (int i=0;i<2;i++) {
+ if (polla[i].revents & POLLERR)
+ return;
+
+ if (polla[i].revents & POLLIN ){
+ read(polla[i].fd, &ev, sizeof(ev));
+ monitor_event(m, &ev);
+ }
+ }
+ } else if (res == 0) {
+ monitor_event(m, &m->repeat);
+ }
+ }
+}
+
+int main(int argc, char **argv) {
+ struct monitor *m = monitor_new();
+
+ if (m) {
+ monitor(m);
+ monitor_free(m);
+ }
+
+ return 0;
+}
--- /dev/null
+/*
+ Copyright (C) 2019 Michael Zucchi
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+*/
+
+#include <libavformat/avformat.h>
+#include <libswresample/swresample.h>
+
+#include <alsa/asoundlib.h>
+#include <poll.h>
+#include <pthread.h>
+
+#include "notify.h"
+#include "ez-list.h"
+#include "ez-port.h"
+#include "dbindex.h"
+
+struct audio_player;
+
+//#define VOICE_MT
+
+/*
+ synchronous voice experiment
+*/
+int init_voice(struct audio_player *ap);
+void close_voice(struct audio_player *ap);
+int voice_start(struct audio_player *ap);
+int voice_speak(struct audio_player *ap, const char *text);
+int voice_finish(struct audio_player *ap);
+int voice_speak_text(struct audio_player *ap, const char *text);
+
+/*
+ async voice stuff
+ */
+struct voice_audio_msg {
+ struct ez_node ln;
+
+ int type;
+ int seq;
+ int more; // more coming?
+
+ int nsamples;
+ short samples[];
+};
+
+int audio_init_voice(struct audio_player *ap);
+int audio_close_voice(struct audio_player *ap);
+int audio_voice_speak(struct audio_player *ap, const char *text);
+static void handle_audio_msg(struct audio_player *ap, struct voice_audio_msg *msg);
+
+#ifndef VOICE_MT
+#define audio_close_voice close_voice
+#define audio_init_voice init_voice
+#define voice_speak_text audio_voice_speak
+#endif
+
+/*
+ Player bits
+*/
+
+int audio_close_media(struct audio_player *ap);
+void audio_close_pcm(struct audio_player *ap);
+
+int audio_init_mixer(struct audio_player *ap);
+
+struct audio_player {
+ notify_t player; // player control port
+
+ // unused
+ //ez_port *reader; // reader info port
+ struct ez_port *player_port; // ? -> player
+
+ char *device;
+ snd_pcm_t *aud; // output device
+ snd_mixer_t *amixer; // volume control
+
+ // voice output
+ pthread_t voice_thread;
+ struct ez_port *voice_port; // player -> voice thread
+ SwrContext *voice_swr;
+ int voice_swr_rate;
+ volatile int voice_seq;
+ // managed by voice thread
+ volatile int voice_rate;
+ volatile int voice_cur_seq;
+
+ // playlist management
+ dbindex *index;
+ dbfile *playing;
+ char *playing_path;
+
+ int quit;
+ int paused;
+ int paused_tmp; // temporary pause, e.g. talking or effects, a count
+
+ // not used
+ // << delete
+ int npoll;
+ int naudio;
+ struct pollfd *poll; // for polling in-port and out-audio
+ int file_seq; // currently active file, hmm, do i care?
+ // delete >>
+
+ // per-file state
+ // << delete
+ pthread_t reader_id;
+ volatile int state; // read by thread to communicate shutdown
+ // delete >>
+
+ AVFormatContext *fc;
+ AVCodecContext *cc;
+ AVStream *audio;
+
+ SwrContext *swr; // if needed for output
+ size_t buffer_size; // for output conversion if required
+ uint8_t *buffer;
+
+ // pre-frame state
+ //AVFrame *frame;// unused?
+ int64_t pos; // current pos in audio stream units
+
+ char *data[1]; // data pointers for current output, planar or not, may point to 'buffer'
+ int nsamples; // nsamples remaining for output
+
+};
+
+int audio_mixer_adjust(struct audio_player *ap, int delta);
+
+struct audio_player *audio_player_new(const char *device) {
+ struct audio_player *ap = calloc(sizeof(*ap), 1);
+
+ ap->player = notify_reader_new(NOTIFY_PLAYER);
+ ap->device = strdup(device);
+ ap->index = dbindex_open(MAIN_INDEX);
+
+ audio_init_voice(ap);
+ audio_init_mixer(ap);
+
+ return ap;
+}
+
+int audio_init_mixer(struct audio_player *ap) {
+ int res;
+
+ res = snd_mixer_open(&ap->amixer, 0);
+ if (res == 0) {
+ res = snd_mixer_attach(ap->amixer, ap->device);
+ res = snd_mixer_selem_register(ap->amixer, NULL, NULL);
+ res = snd_mixer_load(ap->amixer);
+ }
+ return res;
+}
+
+static snd_mixer_elem_t *mixer_master(struct audio_player *ap) {
+ snd_mixer_selem_id_t *sid;
+ const char *selem_name = "Master";
+
+ snd_mixer_selem_id_alloca(&sid);
+ snd_mixer_selem_id_set_index(sid, 0);
+ snd_mixer_selem_id_set_name(sid, selem_name);
+
+ return snd_mixer_find_selem(ap->amixer, sid);
+}
+
+int audio_mixer_mute(struct audio_player *ap) {
+ snd_mixer_elem_t* elem = mixer_master(ap);
+ int mute;
+
+ if (elem) {
+ snd_mixer_selem_get_playback_switch(elem, SND_MIXER_SCHN_MONO, &mute);
+ return snd_mixer_selem_set_playback_switch_all(elem, !mute);
+ } else {
+ return -1;
+ }
+}
+
+int audio_mixer_adjust(struct audio_player *ap, int delta) {
+ snd_mixer_elem_t* elem = mixer_master(ap);
+ long min, max, now;
+ int res;
+
+ if (elem) {
+ res = snd_mixer_selem_get_playback_volume_range(elem, &min, &max);
+ snd_mixer_selem_get_playback_volume(elem, SND_MIXER_SCHN_MONO, &now);
+ now += delta;
+ now = now < min ? min : now;
+ now = now > max ? max : now;
+ snd_mixer_selem_set_playback_volume_all(elem, now);
+ return res;
+ } else {
+ return -1;
+ }
+}
+
+void audio_close_mixer(struct audio_player *ap) {
+ if (ap->amixer) {
+ snd_mixer_close(ap->amixer);
+ ap->amixer = NULL;
+ }
+}
+
+void audio_player_free(struct audio_player *ap) {
+ audio_close_media(ap);
+ audio_close_pcm(ap);
+ audio_close_mixer(ap);
+
+ audio_close_voice(ap);
+
+ dbindex_close(ap->index);
+
+ swr_free(&ap->swr);
+
+ free(ap->poll);
+ free(ap->buffer);
+ free(ap->device);
+ free(ap);
+}
+
+int audio_init_poll(struct audio_player *ap) {
+ int npoll = 1;
+ int count = 0;
+
+ if (ap->aud) {
+ count = snd_pcm_poll_descriptors_count(ap->aud);
+ }
+
+ npoll += count;
+ if (ap->npoll < npoll) {
+ ap->npoll = npoll;
+ ap->naudio = count;
+ ap->poll = realloc(ap->poll, sizeof(*ap->poll) * npoll);
+ }
+
+ // player port
+ ap->poll[0].fd = ap->player;
+ ap->poll[0].events = POLLIN;
+ // reader port
+ //ap->poll[1].fd = ez_port_fd(ap->reader);
+ //ap->poll[1].events = POLLIN;
+
+ if (ap->aud)
+ snd_pcm_poll_descriptors(ap->aud, ap->poll+1, count);
+
+ return 0;
+}
+
+/**
+ * Returns a bitmask of things that are ready.
+ *
+ * bit 0 = player incoming message
+ * bit 1 = reader incoming message
+ * bit 2 = audio output ready
+ * @return -1 on error.
+ */
+int audio_poll(struct audio_player *ap) {
+ int res;
+
+ while (1) {
+ res = poll(ap->poll, ap->npoll, -1);
+ if (res == 0) {
+ unsigned short revents;
+ int state = 0;
+
+ if (ap->poll[0].revents & POLLIN)
+ state |= 1;
+ //if (ap->poll[1].revents & POLLIN)
+ // state |= 2;
+
+ if (ap->poll[0].revents & POLLERR)
+ return -EIO;
+ //if (ap->poll[1].revents & POLLERR)
+ // return -EIO;
+
+ if (ap->aud) {
+ snd_pcm_poll_descriptors_revents(ap->aud, ap->poll+1, ap->naudio, &revents);
+ if (revents & POLLOUT)
+ state |= 4;
+ if (revents & POLLERR)
+ return -EIO;
+ }
+
+ return state;
+ }
+ }
+}
+
+
+static unsigned int hw_sample_rate(struct audio_player *ap) {
+ snd_pcm_hw_params_t *hw;
+ unsigned int actual_rate;
+ int dir;
+ int res;
+
+ snd_pcm_hw_params_alloca(&hw);
+ res = snd_pcm_hw_params_current(ap->aud, hw);
+ res = snd_pcm_hw_params_get_rate(hw, &actual_rate, &dir);
+
+ return actual_rate;
+}
+
+/**
+ * Open, re-open, init and or re-init the audio device for the given sample rate.
+ *
+ * Samples are in stereo interleaved signed 16-bit format.
+ *
+ * The audio device will be in the READY state when this returns successfully.
+ */
+int audio_init_pcm(struct audio_player *ap) {
+ AVCodecContext *cc = ap->cc;
+ int res;
+ int init = 1;
+
+ printf("audio_init_pcm\n");
+
+ if (!ap->aud) {
+ res = snd_pcm_open(&ap->aud, ap->device ? ap->device : "default", SND_PCM_STREAM_PLAYBACK, 0);
+ if (res < 0)
+ return res;
+ } else {
+ if (hw_sample_rate(ap) == cc->sample_rate) {
+ printf(" rate unchanged\n");
+ init = 0;
+ } else if (snd_pcm_state(ap->aud) == SND_PCM_STATE_RUNNING) {
+ // other states?
+ res = snd_pcm_drain(ap->aud);
+ }
+ }
+
+ if (init) {
+ res = snd_pcm_set_params(ap->aud,
+ SND_PCM_FORMAT_S16_LE,
+ SND_PCM_ACCESS_RW_INTERLEAVED,
+ 2,
+ cc->sample_rate,
+ 1,
+ 500000);
+ if (res < 0)
+ return res;
+ }
+
+ // Check actual sample rate of hardware
+ int actual_rate = hw_sample_rate(ap);
+
+ // Create resampler if needed
+ if (cc->sample_fmt != AV_SAMPLE_FMT_S16
+ || cc->channel_layout != AV_CH_LAYOUT_STEREO
+ || cc->sample_rate != actual_rate) {
+ printf(" soft resample %ld:%s:%d -> %d:%s:%d\n",
+ cc->channel_layout, av_get_sample_fmt_name(cc->sample_fmt), cc->sample_rate,
+ AV_CH_LAYOUT_STEREO, av_get_sample_fmt_name(AV_SAMPLE_FMT_S16), actual_rate);
+
+ ap->swr = swr_alloc_set_opts(ap->swr,
+ AV_CH_LAYOUT_STEREO, // out_ch_layout
+ AV_SAMPLE_FMT_S16, // out_sample_fmt
+ actual_rate, // out_sample_rate
+ cc->channel_layout, // in_ch_layout
+ cc->sample_fmt, // in_sample_fmt
+ cc->sample_rate, // in_sample_rate
+ 0, // log_offset
+ NULL); // log_ctx
+ if (ap->swr == NULL) {
+ printf("swr create failed\n");
+ return -1;
+ }
+ swr_init(ap->swr);
+ } else {
+ swr_free(&ap->swr);
+ }
+
+ return audio_init_poll(ap);
+}
+
+int audio_stop_pcm(struct audio_player *ap) {
+ int res;
+
+ if (ap->paused) {
+ ap->paused = 0;
+ snd_pcm_drop(ap->aud);
+ res = snd_pcm_pause(ap->aud, 0);
+ }
+
+ res = snd_pcm_drain(ap->aud);
+
+ return res;
+}
+
+void audio_close_pcm(struct audio_player *ap) {
+ int res;
+
+ snd_pcm_drop(ap->aud);
+ if (ap->paused) {
+ res = snd_pcm_pause(ap->aud, 0);
+ }
+ res = snd_pcm_drain(ap->aud);
+ snd_pcm_close(ap->aud);
+ ap->aud = NULL;
+}
+
+/**
+ * New frame ready.
+ * Set up pointers for pcm output.
+ */
+int audio_init_frame(struct audio_player *ap, AVFrame *frame) {
+ if (ap->swr) {
+ int samples = swr_get_out_samples(ap->swr, frame->nb_samples);
+
+ if (ap->buffer_size < samples * 4) {
+ ap->buffer_size = samples * 4;
+ ap->buffer = realloc(ap->buffer, ap->buffer_size);
+ }
+ ap->nsamples = swr_convert(ap->swr, &ap->buffer, samples, (const uint8_t **)frame->data, frame->nb_samples);
+ ap->data[0] = (void *)ap->buffer;
+ } else {
+ ap->data[0] = (void *)frame->data[0];
+ ap->nsamples = frame->nb_samples;
+ }
+ ap->pos = frame->pts;
+
+ return 0;
+}
+
+int audio_send_pcm(struct audio_player *ap) {
+ snd_pcm_sframes_t sent;
+
+ sent = snd_pcm_writei(ap->aud, ap->data[0], ap->nsamples);
+
+ if (sent > 0) {
+ ap->nsamples -= sent;
+ ap->data[0] += sent * 4;
+ }
+
+ if (sent < 0) {
+ printf("snd_pcm_write failed (state=%s): %s\n", snd_pcm_state_name(snd_pcm_state(ap->aud)), snd_strerror(sent));
+ sent = snd_pcm_recover(ap->aud, sent, 0);
+ }
+
+ if (sent < 0) {
+ printf("snd_pcm_write failed: %s\n", snd_strerror(sent));
+ if (sent != EAGAIN || sent != EINTR) {
+ ap->nsamples = 0;
+ }
+ }
+
+ return sent;
+}
+
+int audio_close_media(struct audio_player *ap) {
+ avcodec_free_context(&ap->cc);
+ avformat_close_input(&ap->fc);
+ ap->audio = NULL;
+
+ // ?? or do we want to flush them?
+ ap->nsamples = 0;
+ return 0;
+}
+
+int audio_init_media(struct audio_player *ap, const char *path) {
+ int res;
+ AVCodec *codec;
+ int audioid;
+
+ printf("audio_init_media '%s'\n", path);
+
+ audio_close_media(ap);
+
+ ap->fc = avformat_alloc_context();
+ res = avformat_open_input(&ap->fc, path, NULL, NULL);
+ if (res != 0)
+ goto fail;
+
+ res = avformat_find_stream_info(ap->fc, NULL);
+ if (res != 0)
+ goto fail;
+
+ av_dump_format(ap->fc, 0, path, 0);
+
+ audioid = av_find_best_stream(ap->fc, AVMEDIA_TYPE_AUDIO, -1, -1, &codec, 0);
+ if (audioid < 0)
+ goto fail;
+
+ for (int i=0;i<ap->fc->nb_streams;i++)
+ ap->fc->streams[i]->discard = i == audioid ? AVDISCARD_DEFAULT : AVDISCARD_ALL;
+
+ ap->audio = ap->fc->streams[audioid];
+ ap->cc = avcodec_alloc_context3(NULL);
+ res = avcodec_parameters_to_context(ap->cc, ap->audio->codecpar);
+ ap->cc->codec_id = codec->id;
+
+ res = avcodec_open2(ap->cc, codec, NULL);
+ if (res < 0)
+ goto fail;
+
+ printf("codec rate: %d\n", ap->cc->sample_rate);
+ printf("codec channels: %d\n", ap->cc->channels);
+ printf("codec layout: %lu\n", ap->cc->channel_layout);
+ printf("codec format: %s\n", av_get_sample_fmt_name(ap->cc->sample_fmt));
+
+ return audio_init_pcm(ap);
+ fail:
+ perror("init media");
+ audio_close_media(ap);
+ return -1;
+}
+
+// Next in play queue
+int audio_next_file(struct audio_player *ap) {
+ int res;
+ int empty = ap->playing == NULL;
+
+ audio_close_media(ap);
+ do {
+ //res = dbfile_next(ap->index, &ap->playing, &ap->playing_path);
+ printf("a ap playing = %d\n", ap->playing ? ap->playing->id : -1);
+ res = dbfile_next_shuffle(ap->index, &ap->playing, &ap->playing_path);
+ printf("b ap playing = %d\n", ap->playing ? ap->playing->id : -1);
+
+ if (res == 0)
+ res = audio_init_media(ap, ap->playing_path);
+ if (res == (-30798) && !empty) // && >loop?
+ res = 1;
+ } while (res != 0 && res !=(-30798)); // MDB_NOTFOUND
+
+ //if (res != 0) {
+ // audio_stop_pcm(ap);
+ //}
+
+ return res;
+}
+
+int audio_prev_file(struct audio_player *ap) {
+ int res;
+ int empty = ap->playing == NULL;
+
+ audio_close_media(ap);
+ do {
+ //res = dbfile_prev(ap->index, &ap->playing, &ap->playing_path);
+ res = dbfile_prev_shuffle(ap->index, &ap->playing, &ap->playing_path);
+ if (res == 0)
+ res = audio_init_media(ap, ap->playing_path);
+ if (res == (-30798) && !empty) // && >loop?
+ res = 1;
+ } while (res != 0 && res !=(-30798)); // MDB_NOTFOUND
+
+ //if (res != 0) {
+ // audio_stop_pcm(ap);
+ //}
+
+ return res;
+}
+
+
+void audio_player_control(struct audio_player *ap) {
+ int ready = notify_msg_ready(ap->player);
+
+ mainloop:
+ while (!ap->quit
+ && (ap->cc == NULL || ap->paused || ready || ap->paused_tmp)) {
+ int res;
+ void *msg;
+ enum notify_action action;
+ unsigned int pri;
+
+ if (ap->paused)
+ printf("player: paused\n");
+ else if (!ready)
+ printf("player: idle\n");
+
+ // temporary stuff testing async voice
+ // this will monitor the player port and handle any messages that arrive
+ // but quit the loop if paused_tmp gets cleared by handle_audio_msg
+#ifdef VOICE_MT
+ int wait = 0;
+ do {
+ struct pollfd fds[2];
+
+ fds[0].fd = ap->player;
+ fds[0].events = POLLIN;
+ fds[1].fd = ez_port_fd(ap->player_port);
+ fds[1].events = POLLIN;
+
+ res = poll(fds, 2, -1); // timeout to check stuff?
+
+ if (fds[0].revents & POLLERR
+ || fds[0].revents & POLLERR) {
+ // what now?
+
+ }
+
+ if (fds[1].revents & POLLIN) {
+ handle_audio_msg(ap, ez_port_take(ap->player_port));
+ }
+ wait = (fds[0].revents & POLLIN) == 0;
+
+ if (wait && !ap->paused_tmp)
+ goto mainloop;
+ } while (wait);
+#endif
+
+ msg = notify_msg_receive(ap->player, &action, &pri);
+ if (ready > 0)
+ ready -= 1;
+
+ printf("msg %d\n", action);
+
+ switch (action) {
+ case NOTIFY_QUIT:
+ ap->quit = 1;
+ return;
+ case NOTIFY_PLAY_PLAY:
+ if (ap->paused) {
+ res = snd_pcm_pause(ap->aud, 0);
+ ap->paused = 0;
+ }
+ break;
+ case NOTIFY_PLAY_PAUSE:
+ if (ap->cc) {
+ ap->paused ^= 1;
+ res = snd_pcm_pause(ap->aud, ap->paused);
+ }
+ break;
+ case NOTIFY_PLAY_STOP:
+ // something something
+ break;
+ case NOTIFY_PLAY_SEEK:
+ if (ap->cc) {
+ struct notify_play_seek *s = msg;
+ double now = ap->pos * av_q2d(ap->audio->time_base);
+
+ if (s->mode == 1) {
+ double rel = s->stamp;
+ int64_t seek = (int64_t)((now + rel) * AV_TIME_BASE);
+ int64_t min = rel > 0 ? (int64_t)((now - rel) * AV_TIME_BASE) + 2 : INT64_MIN;
+ int64_t max = rel < 0 ? (int64_t)((now - rel) * AV_TIME_BASE) - 2 : INT64_MAX;
+
+ printf("seek %f%+f %ld %ld %ld\n", now, rel, min, seek, max);
+ res = avformat_seek_file(ap->fc, -1, min, seek, max, 0);
+ } else {
+ double rel = s->stamp;
+ int64_t seek = (int64_t)((rel) * AV_TIME_BASE);
+ int64_t min = rel > now ? seek - 2 : INT64_MIN;
+ int64_t max = rel < now ? seek + 2 : INT64_MAX;
+
+ printf("seek %f %ld %ld %ld\n", rel, min, seek, max);
+ res = avformat_seek_file(ap->fc, -1, min, seek, max, 0);
+ }
+ avcodec_flush_buffers(ap->cc);
+ }
+ break;
+ case NOTIFY_PLAY_NEXT:
+ audio_next_file(ap);
+ break;
+ case NOTIFY_PLAY_PREV:
+ audio_prev_file(ap);
+ break;
+ case NOTIFY_VOLUME_UP:
+ audio_mixer_adjust(ap, +1);
+ break;
+ case NOTIFY_VOLUME_DOWN:
+ audio_mixer_adjust(ap, -1);
+ break;
+ case NOTIFY_VOLUME_MUTE:
+ audio_mixer_mute(ap);
+ break;
+ case NOTIFY_KEY: {
+ /*
+ Hmm, this might get ugly fast, is there another way?
+
+ */
+ struct notify_key *k = msg;
+ switch (k->code) {
+ case 139://case KEY_MENU:
+ // start menu
+ ap->voice_seq += 1;
+ audio_voice_speak(ap, "Main Menu. Play-list, Help, System.");
+ break;
+ case 138://case KEY_HELP:
+ // say current file
+ // could also get info from the AVFormatContext metadata.
+ if (ap->playing) {
+ audio_voice_speak(ap, "Playing.\n\n");
+ if (strcmp(ap->playing->title, "Unknown") == 0) {
+ audio_voice_speak(ap, "File.\n\n");
+ audio_voice_speak(ap, strrchr(ap->playing->path, '/')+1);
+ } else {
+ audio_voice_speak(ap, ap->playing->title);
+ if (strcmp(ap->playing->artist, "Unknown") != 0) {
+ audio_voice_speak(ap, "by\n\n");
+ audio_voice_speak(ap, ap->playing->artist);
+ }
+ }
+ } else {
+ audio_voice_speak(ap, "Player Idle");
+ }
+ break;
+ }
+ break;
+ }
+ case NOTIFY_DEBUG: {
+ struct notify_debug *d = msg;
+
+ switch (d->func) {
+ case 0: {
+ printf("pre-drain: %d\n", snd_pcm_state(ap->aud));
+ snd_pcm_drain(ap->aud);
+ printf("pst-drain: %d\n", snd_pcm_state(ap->aud));
+
+ // try to re-init the stream
+ res = snd_pcm_set_params(ap->aud,
+ SND_PCM_FORMAT_S16_LE,
+ SND_PCM_ACCESS_RW_INTERLEAVED,
+ 2,
+ //frame->sample_rate,
+ 48000,
+ 1,
+ 500000);
+ if (res < 0) {
+ printf("snd_pcm_set_params: %s\n", snd_strerror(res));
+ exit(1);
+ }
+ break;
+ }
+ case 1: {
+ // dump some info
+ snd_pcm_hw_params_t *hw;
+ snd_output_t *out = NULL;
+
+ snd_pcm_hw_params_alloca(&hw);
+ res = snd_pcm_hw_params_current(ap->aud, hw);
+ printf("snd_pcm_hw_params_current: %s\n", snd_strerror(res));
+ res = snd_output_stdio_attach(&out, stdout, 0);
+ printf("snd_output_stdio_attach: %s\n", snd_strerror(res));
+ snd_pcm_hw_params_dump(hw, out);
+ break;
+ }
+ case 2:
+ // cause an underrun?
+ sleep(2);
+ break;
+ case 3:
+ // exit (mem test)
+ ap->quit = 1;
+ break;
+ }
+ break;
+ }
+ default:
+ break;
+ }
+ notify_msg_free(action, msg);
+ }
+}
+
+// TODO: the play queue / play list / etc
+void audio_player_loop(struct audio_player *ap) {
+ AVFrame *frame = av_frame_alloc();
+
+ audio_next_file(ap);
+
+ while (1) {
+ int res;
+
+ // Handle any commands
+ audio_player_control(ap);
+
+ if (ap->quit)
+ break;
+
+ // Flush any remaining samples first
+ if (ap->nsamples) {
+ res = audio_send_pcm(ap);
+ continue;
+ }
+
+ // Have file open / reading
+ if (ap->cc) {
+ AVPacket pkt;
+
+ res = avcodec_receive_frame(ap->cc, frame);
+ if (res >= 0) {
+ res = audio_init_frame(ap, frame);
+ res = audio_send_pcm(ap);
+ continue;
+ }
+
+ res = av_read_frame(ap->fc, &pkt);
+ if (res < 0) {
+ audio_close_media(ap);
+ audio_next_file(ap);
+ continue;
+ }
+
+ avcodec_send_packet(ap->cc, &pkt);
+ av_packet_unref(&pkt);
+ }
+ }
+
+ av_frame_free(&frame);
+
+ // shutdown shit
+}
+
+int main(int argc, char **argv) {
+ struct audio_player *ap;
+ char *device = "plug:dmix";
+
+ device = "default";
+
+ ap = audio_player_new(device);
+ audio_player_loop(ap);
+ audio_player_free(ap);
+
+ snd_config_update_free_global();
+
+ return 0;
+}
+
+
+/*
+ prototyping area
+*/
+
+/*
+
+ This needs another thread.
+
+ Thread starts synthesising, sends audio to music player via throttled message port(?)
+
+ */
+
+#include <espeak-ng/speak_lib.h>
+
+#ifdef VOICE_MT
+struct voice_msg {
+ int type;
+ int seq;
+ char text[];
+};
+
+static void handle_audio_msg(struct audio_player *ap, struct voice_audio_msg *msg) {
+ if (msg->type == 0) {
+ printf("recv: synth samples %d\n", (int)msg->nsamples);
+ if (msg->seq == ap->voice_seq) {
+ int actual_rate = hw_sample_rate(ap);
+
+ if (ap->voice_swr == NULL || actual_rate != ap->voice_swr_rate) {
+ ap->voice_swr = swr_alloc_set_opts
+ (ap->voice_swr,
+ AV_CH_LAYOUT_STEREO, // out_ch_layout
+ AV_SAMPLE_FMT_S16, // out_sample_fmt
+ actual_rate, // out_sample_rate
+ AV_CH_LAYOUT_MONO, // in_ch_layout
+ AV_SAMPLE_FMT_S16, // in_sample_fmt
+ ap->voice_rate, // in_sample_rate
+ 0, // log_offset
+ NULL); // log_ctx
+ ap->voice_swr_rate = actual_rate;
+ swr_init(ap->voice_swr);
+ }
+
+ int nsamples = msg->nsamples;
+ void *data = msg->samples;
+ int samples = swr_get_out_samples(ap->voice_swr, nsamples);
+
+ if (ap->buffer_size < samples * 4) {
+ ap->buffer_size = samples * 4;
+ ap->buffer = realloc(ap->buffer, ap->buffer_size);
+ }
+ ap->nsamples = swr_convert(ap->voice_swr, &ap->buffer, samples, (const uint8_t **)&data, nsamples);
+ ap->data[0] = (void *)ap->buffer;
+
+ // does it need flushing in voice_finish()?
+
+ audio_send_pcm(ap);
+ } else {
+ printf("discard stale voice audio\n");
+ }
+ } else if (msg->type == 1) {
+ printf("recv: synth end samples\n");
+ ap->paused_tmp -= 1;
+ }
+ free(msg);
+}
+
+
+static int voice_thread_data(short *data, int nsamples, espeak_EVENT *event) {
+ struct audio_player *ap = event->user_data;
+
+ if (!nsamples)
+ return 0;
+
+ if (ap->voice_seq != ap->voice_cur_seq) {
+ printf("synth: abort decode\n");
+ return -1;
+ }
+
+ struct voice_audio_msg *msg = malloc(sizeof(*msg) + nsamples * sizeof(short));
+
+ msg->type = 0;
+ msg->seq = ap->voice_cur_seq;
+ msg->nsamples = nsamples;
+ memcpy(msg->samples, data, nsamples * sizeof(short));
+
+ ez_port_put(ap->player_port, msg);
+
+ return 0;
+}
+
+static void *voice_thread(void *d) {
+ struct audio_player *ap = d;
+ struct voice_msg *msg;
+ int go = 1;
+
+ ap->voice_rate = espeak_Initialize(AUDIO_OUTPUT_RETRIEVAL, 120, NULL, espeakINITIALIZE_DONT_EXIT);
+
+ // God it talks like a maniac, slow it down a bit
+ espeak_SetSynthCallback(voice_thread_data);
+ espeak_SetParameter(espeakRATE, 160, 0);
+ espeak_SetParameter(espeakWORDGAP, 15, 0);
+
+ printf("Voice: thread started\n");
+
+ while (go && (msg = ez_port_take(ap->voice_port))) {
+ switch (msg->type) {
+ case 0:
+ go = 0;
+ break;
+ case 1:
+ if (ap->voice_seq == msq->seq) {
+ ap->voice_cur_seq = msg->seq;
+
+ printf("send: synth startn");
+
+ espeak_Synth(msg->text, strlen(msg->text)+1, 0, POS_CHARACTER, 0, espeakCHARS_8BIT | espeakPHONEMES, NULL, ap);
+
+ struct voice_audio_msg *amsg = calloc(sizeof(*amsg), 1);
+
+ printf("send: synth end\n");
+ amsg->type = 1;
+ amsg->seq = msg->seq;
+ ez_port_put(ap->player_port, amsg);
+ }
+ break;
+ }
+ free(msg);
+ }
+
+ espeak_Terminate();
+
+ printf("Voice: thread done\n");
+
+ return d;
+}
+
+int audio_init_voice(struct audio_player *ap) {
+ ap->voice_port = ez_port_new(8);
+ ap->player_port = ez_port_new(1);
+
+ pthread_create(&ap->voice_thread, NULL, voice_thread, ap);
+
+ return 0;
+}
+
+int audio_close_voice(struct audio_player *ap) {
+ pthread_join(ap->voice_thread, NULL);
+ ez_port_free(ap->voice_port);
+ ez_port_free(ap->player_port);
+ return 0;
+}
+
+int audio_voice_speak(struct audio_player *ap, const char *text) {
+ struct voice_msg *msg;
+
+ msg = malloc(sizeof(*msg) + strlen(text)+1);
+
+ msg->type = 1;
+ msg->seq = ap->voice_seq;
+ strcpy(msg->text, text);
+
+ ez_port_put(ap->voice_port, msg);
+
+ ap->paused_tmp += 1;
+
+ return 0;
+}
+
+#else
+
+static int voice_rate;
+static struct audio_player *player;
+//static snd_pcm_hw_params_t *voice_save;
+static SwrContext *voice_swr;
+
+static int voice_data(short *data, int nsamples, espeak_EVENT*event) {
+ struct audio_player *ap = player;
+
+ printf("samples: %d\n", nsamples);
+ for (int i=0;event[i].type;i++)
+ printf(" event: %d\n", event[i].type);
+
+ int samples = swr_get_out_samples(voice_swr, nsamples);
+
+ if (ap->buffer_size < samples * 4) {
+ ap->buffer_size = samples * 4;
+ ap->buffer = realloc(ap->buffer, ap->buffer_size);
+ }
+ ap->nsamples = swr_convert(voice_swr, &ap->buffer, samples, (const uint8_t **)&data, nsamples);
+ ap->data[0] = (void *)ap->buffer;
+
+ // does it need flushing in voice_finish()?
+
+ audio_send_pcm(ap);
+
+ return 0;
+}
+
+int init_voice(struct audio_player *ap) {
+ voice_rate = espeak_Initialize(AUDIO_OUTPUT_RETRIEVAL, 0, NULL, espeakINITIALIZE_DONT_EXIT);
+
+ if (voice_rate <= 0)
+ return -1;
+
+ // God it talks like a maniac, slow it down a bit
+ espeak_SetSynthCallback(voice_data);
+ espeak_SetParameter(espeakRATE, 160, 0);
+ espeak_SetParameter(espeakWORDGAP, 15, 0);
+
+ player = ap;
+
+ return 0;
+}
+
+void close_voice(struct audio_player *ap) {
+ espeak_Terminate();
+ player = NULL;
+}
+
+int voice_start(struct audio_player *ap) {
+ int actual_rate = hw_sample_rate(ap);
+
+ if (actual_rate != voice_rate) {
+ voice_swr = swr_alloc_set_opts(voice_swr,
+ AV_CH_LAYOUT_STEREO, // out_ch_layout
+ AV_SAMPLE_FMT_S16, // out_sample_fmt
+ actual_rate, // out_sample_rate
+ AV_CH_LAYOUT_MONO, // in_ch_layout
+ AV_SAMPLE_FMT_S16, // in_sample_fmt
+ voice_rate, // in_sample_rate
+ 0, // log_offset
+ NULL); // log_ctx
+ swr_init(voice_swr);
+ }
+
+ return 0;
+}
+
+int voice_finish(struct audio_player *ap) {
+ return 0;
+}
+
+int voice_speak_text(struct audio_player *ap, const char *text) {
+ voice_start(ap);
+ voice_speak(ap, text);
+ voice_finish(ap);
+ return 0;
+}
+
+int voice_speak(struct audio_player *ap, const char *text) {
+ espeak_Synth(text, strlen(text), 0, POS_CHARACTER, 0, espeakCHARS_8BIT | espeakPHONEMES, NULL, NULL);
+ return 0;
+}
+#endif
--- /dev/null
+
+#include <fcntl.h>
+#include <sys/stat.h>
+#include <mqueue.h>
+
+#include <poll.h>
+
+#include <stdint.h>
+#include <stdio.h>
+#include <errno.h>
+
+#include "dbindex.h"
+
+#include "notify.h"
+
+// Message handling and routing
+
+static ez_blob_desc PLAY_SEEK_DESC[] = {
+ EZ_BLOB_START(struct notify_play_seek),
+ EZ_BLOB_INT32(struct notify_play_seek, 1, mode),
+ EZ_BLOB_FLOAT64(struct notify_play_seek, 2, stamp),
+ EZ_BLOB_END(struct notify_play_seek)
+};
+
+static ez_blob_desc DEBUG_DESC[] = {
+ EZ_BLOB_START(struct notify_debug),
+ EZ_BLOB_INT32(struct notify_debug, 1, func),
+ EZ_BLOB_END(struct notify_debug)
+};
+
+static ez_blob_desc KEY_DESC[] = {
+ EZ_BLOB_START(struct notify_key),
+ EZ_BLOB_INT32(struct notify_key, 1, code),
+ EZ_BLOB_END(struct notify_key)
+};
+
+/**
+ * Blob for action.
+ * This must match enum notify_action.
+ */
+static ez_blob_desc *action_desc[] = {
+ NULL,
+
+ DBDISK_DESC,
+ DBDISK_DESC,
+
+ NULL,
+ NULL,
+ NULL,
+ PLAY_SEEK_DESC,
+
+ NULL,
+ NULL,
+
+ NULL,
+ NULL,
+ NULL,
+
+ NULL,
+
+ KEY_DESC,
+
+ DEBUG_DESC,
+
+ NULL
+};
+
+// should be global by default
+// could force a smaller one i guess
+static long msg_size;
+
+/**
+ * Create a reader queue, these are blocking.
+ */
+mqd_t notify_reader_new(const char *path) {
+ mqd_t q = mq_open(path, O_RDONLY | O_CREAT, 0600, NULL);
+
+ if (q != -1) {
+ struct mq_attr at;
+ mq_getattr(q, &at);
+ msg_size = at.mq_msgsize;
+ }
+
+ return q;
+}
+
+/**
+ * Create a writer queue, these are non-blocking.
+ */
+mqd_t notify_writer_new(const char *path) {
+ mqd_t q = mq_open(path, O_WRONLY | O_CREAT | O_NONBLOCK, 0600, NULL);
+
+ if (q != -1) {
+ struct mq_attr at;
+ mq_getattr(q, &at);
+ msg_size = at.mq_msgsize;
+ }
+
+ return q;
+}
+
+void notify_close(mqd_t q) {
+ mq_close(q);
+}
+
+void notify_msg_free(enum notify_action action, void *p) {
+ if (action_desc[action]) {
+ ez_blob_free(action_desc[action], p);
+ }
+}
+
+/**
+ * Return the number of messages waiting.
+ */
+int notify_msg_ready(notify_t q) {
+ struct mq_attr at;
+
+ mq_getattr(q, &at);
+ return (int)at.mq_curmsgs;
+}
+
+/**
+ * Send a message. This does not block if the queue is full.
+ *
+ * @return -1 on error, EGAIN means the queue is full.
+ */
+int notify_msg_send(mqd_t q, enum notify_action action, unsigned int msg_pri, const void *p) {
+ int res = -1;
+
+ if (action < NOTIFY_SIZEOF) {
+ printf("send action %d\n", action);
+ if (action_desc[action]) {
+ size_t size = ez_blob_size(action_desc[action], p);
+ char msg[size+1];
+
+ msg[0] = action;
+ ez_blob_encode_raw(action_desc[action], p, msg+1, size);
+ res = mq_send(q, msg, size+1, msg_pri);
+ } else {
+ res = mq_send(q, (char *)&action, 1, msg_pri);
+ }
+ if (res) {
+ perror("msg_send");
+ }
+ } else {
+ errno = EINVAL;
+ perror("msg_send: invalid action");
+ }
+
+ return res;
+}
+
+/**
+ * Recieve a message, blocking until one is available.
+ *
+ * @return the (decoded) message, check actionp to determine it's
+ * format. This may be NULL for no-payload messages or on fatal
+ * error, check the action.
+ */
+void *notify_msg_receive(mqd_t q, enum notify_action *actionp, unsigned int *msg_pri) {
+ char msg[msg_size];
+
+ while (1) {
+ ssize_t size = mq_receive(q, msg, msg_size, msg_pri);
+
+ if (size >= 1) {
+ enum notify_action action = msg[0];
+
+ if (action < NOTIFY_SIZEOF) {
+ *actionp = action;
+
+ if (action_desc[action]) {
+ void *p = ez_blob_decode(action_desc[action], msg+1, size-1);
+
+ if (p)
+ return p;
+ } else {
+ return NULL;
+ }
+ }
+ } else if (size == 0) {
+ // ignore or quit?
+ } else {
+ if (errno == EINTR)
+ ;
+ else {
+ perror("server: mq_receive");
+ break;
+ }
+ }
+ }
+
+ *actionp = NOTIFY_QUIT;
+
+ return NULL;
+}
--- /dev/null
+
+#ifndef _NOTIFY_H
+#define _NOTIFY_H
+
+#define NOTIFY_BROKER "/pz.broker"
+#define NOTIFY_INDEXER "/pz.indexer"
+#define NOTIFY_PLAYER "/pz.player"
+
+enum notify_action {
+ NOTIFY_QUIT,
+
+ NOTIFY_DISK_ADD,
+ NOTIFY_DISK_REMOVE,
+
+ NOTIFY_PLAY_PLAY,
+ NOTIFY_PLAY_PAUSE,
+ NOTIFY_PLAY_STOP,
+ NOTIFY_PLAY_SEEK, // notify_play_seek : absolute, fixed, forward or rewind
+
+ NOTIFY_PLAY_NEXT,
+ NOTIFY_PLAY_PREV,
+
+ NOTIFY_VOLUME_UP,
+ NOTIFY_VOLUME_DOWN,
+ NOTIFY_VOLUME_MUTE,
+
+ NOTIFY_TRACK_INFO, // etc?
+
+ NOTIFY_KEY, // some key pressed
+
+ NOTIFY_DEBUG, // debug/prototyping command
+
+ NOTIFY_SHUFFLE, /* disk-manager: create shuffled playlist */
+
+ NOTIFY_SIZEOF
+};
+
+typedef int notify_t;
+
+struct notify_play_seek {
+ int mode; // 0 abs, 1 relative
+ double stamp;
+};
+
+struct notify_debug {
+ int func;
+};
+
+struct notify_key {
+ int code;
+};
+
+notify_t notify_reader_new(const char *path);
+notify_t notify_writer_new(const char *path);
+void notify_close(notify_t q);
+
+int notify_msg_ready(notify_t q);
+
+int notify_msg_send(notify_t q, enum notify_action action, unsigned int msg_pri, const void *payload);
+void *notify_msg_receive(notify_t q, enum notify_action *actionp, unsigned int *msg_pri);
+
+void notify_msg_free(enum notify_action action, void *p);
+
+#endif