From: Not Zed Date: Wed, 18 Nov 2020 03:20:35 +0000 (+1030) Subject: Initial import of music player. X-Git-Url: https://code.zedzone.au/cvs?a=commitdiff_plain;h=4fb7a415dd41f8ce75c6f39b3ebe95d3871ca4ed;p=playerz Initial import of music player. --- 4fb7a415dd41f8ce75c6f39b3ebe95d3871ca4ed diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..8afde7f --- /dev/null +++ b/Makefile @@ -0,0 +1,42 @@ + +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 diff --git a/audio-cmd.c b/audio-cmd.c new file mode 100644 index 0000000..87e74a9 --- /dev/null +++ b/audio-cmd.c @@ -0,0 +1,57 @@ + +#include +#include +#include + +#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); + + } +} diff --git a/dbindex.c b/dbindex.c new file mode 100644 index 0000000..2cfd282 --- /dev/null +++ b/dbindex.c @@ -0,0 +1,1010 @@ +#include +#include +#include +#include +#include + +#include + +#include + +#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;ishuffle, &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 +#include +#include +#include + +/** + * 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); + +*/ diff --git a/dbindex.h b/dbindex.h new file mode 100644 index 0000000..21a5e56 --- /dev/null +++ b/dbindex.h @@ -0,0 +1,70 @@ + +#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" diff --git a/disk-indexer.c b/disk-indexer.c new file mode 100644 index 0000000..84987e2 --- /dev/null +++ b/disk-indexer.c @@ -0,0 +1,640 @@ + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include + +#include + +#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;icount;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 && inb_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 +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include + +#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;iindexer, 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(); +} diff --git a/input-monitor.c b/input-monitor.c new file mode 100644 index 0000000..8393e2d --- /dev/null +++ b/input-monitor.c @@ -0,0 +1,266 @@ +/** + 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 +#include +#include +#include +#include + +#include + +#include +#include +#include +#include + +#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; +} diff --git a/music-player.c b/music-player.c new file mode 100644 index 0000000..f22632b --- /dev/null +++ b/music-player.c @@ -0,0 +1,1084 @@ +/* + 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 . + +*/ + +#include +#include + +#include +#include +#include + +#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;ifc->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 + +#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 diff --git a/notify.c b/notify.c new file mode 100644 index 0000000..c9a1d31 --- /dev/null +++ b/notify.c @@ -0,0 +1,196 @@ + +#include +#include +#include + +#include + +#include +#include +#include + +#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; +} diff --git a/notify.h b/notify.h new file mode 100644 index 0000000..b6cc3a2 --- /dev/null +++ b/notify.h @@ -0,0 +1,64 @@ + +#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