/* ntk.c */ #include #include #include #include #include /* TODO: * - Slave interpreter for expression evaluation? * - Mixer. */ typedef signed short sample_t; /* Sample type. */ #define SAMPRATE 22050 /* Samples per second. */ #define CHANNELS 1 /* Simultaneous samples. */ #define SAMPBYTE sizeof(sample_t) /* Bytes per sample. */ #define SAMPBITS (SAMPBYTE * 8) /* Bits per sample. */ #define SAMPAMPL ((1LL << (SAMPBITS - 1)) - 1) /* Amplitude. */ typedef struct ntk_client_data { Tcl_Obj* context_dict ; Tcl_Obj* start_key ; Tcl_Obj* phase_key ; Tcl_Obj* duration_key ; Tcl_Obj* position_key ; Tcl_Obj* waveform_key ; Tcl_Obj* amplitude_key; Tcl_Obj* ampl_envl_key; Tcl_Obj* frequency_key; Tcl_Obj* freq_envl_key; } ntk_client_data_t; static int ntk_func(ClientData, Tcl_Interp*, Tcl_Value*, Tcl_Value*); static int ntk_gen_cmd(ClientData, Tcl_Interp*, int, Tcl_Obj* const[]); static int ntk_ctx_cmd(ClientData, Tcl_Interp*, int, Tcl_Obj* const[]); static int ntk_mix_cmd(ClientData, Tcl_Interp*, int, Tcl_Obj* const[]); static void ntk_gen_cleanup(ClientData); static int ntk_func(ClientData value, Tcl_Interp* interp, Tcl_Value* args, Tcl_Value* result) { if (value != NULL) { result->type = TCL_DOUBLE; result->doubleValue = *(double*)value; return TCL_OK; } else { Tcl_SetResult(interp, "functino must be used inside an expression " "evaluated by ::ntk::gen", TCL_STATIC); return TCL_ERROR; } } /* Usage: * * gen num_samps dict_var * * This command generates and returns $num_samps samples. $dict_var is the * name of a dict variable in the caller's stack frame containing the following * keys: * * - start - phase - duration - generator - position * - waveform - amplitude - ampl_envl - frequency - freq_envl * * Each sample is ($waveform * $amplitude * $ampl_envl). The instantaneous * frequency is ($frequency * $freq_envl). $position counts from 0 to * $duration. */ static int ntk_gen_cmd(ClientData client_data, Tcl_Interp* interp, int objc, Tcl_Obj* const objv[]) { ntk_client_data_t* data = client_data; int code ; /* Return code. */ Tcl_Obj * result ; /* Return value, as a Tcl object. */ char * bytes ; /* Return value, as a byte array. */ sample_t* samples ; /* Return value, as a sample array. */ int sample_count ; /* Number of samples to generate. */ int sample_index ; /* Index of current sample. */ char * dict_name; /* Name of the dict variable. */ Tcl_Obj* dict ; /* The dict itself. */ Tcl_Obj* dict_val ; /* Temp. object read from the dict. */ Tcl_Obj* wave_expr; double wave_inst; /* Waveform. */ Tcl_Obj* ampl_expr; double ampl_base, ampl_inst; /* Amplitude. */ Tcl_Obj* freq_expr; double freq_base, freq_inst; /* Frequency. */ double dur ; /* Duration in seconds. */ double pos ; /* Current position (0 through 1). */ double time ; /* Current time in seconds. */ double phase ; /* Phase (0 through 2*pi). */ double dpos ; /* Per-sample adjustment to pos. */ int ipos ; /* Current position in samples. */ int i; /* Everpresent iterator variable. */ /* Mapping from dict keys to local variables. */ struct { Tcl_Obj* key; /* String object used as the key. */ void * val; /* Local variable to store value. */ enum { NTK_TCL_OBJ,/* Tcl_Obj* */ NTK_DOUBLE ,/* double */ NTK_INT /* int */ } type; /* Variable type indicated by val. */ } dict_map[] = { {data->start_key , &time , NTK_DOUBLE }, {data->phase_key , &phase , NTK_DOUBLE }, {data->duration_key , &dur , NTK_DOUBLE }, {data->position_key , &ipos , NTK_INT }, {data->waveform_key , &wave_expr, NTK_TCL_OBJ}, {data->amplitude_key, &l_base, NTK_DOUBLE }, {data->ampl_envl_key, &l_expr, NTK_TCL_OBJ}, {data->frequency_key, &freq_base, NTK_DOUBLE }, {data->freq_envl_key, &freq_expr, NTK_TCL_OBJ} }; /* Ensure the proper number of arguments. */ if (objc != 3) { Tcl_WrongNumArgs(interp, 1, objv, "num_samps dict_var"); return TCL_ERROR; } /* Get the number of samples to be generated. */ if (Tcl_GetIntFromObj(interp, objv[1], &sample_count) == TCL_ERROR) { return TCL_ERROR; } /* Get the dictionary. */ dict_name = Tcl_GetString(objv[2]); dict = Tcl_GetVar2Ex(interp, dict_name, NULL, TCL_LEAVE_ERR_MSG); if (dict == NULL) { return TCL_ERROR; } /* Load the contents of the dictionary into local variables. */ for (i = 0; i < sizeof(dict_map) / sizeof(*dict_map); ++i) { /* Get the value from the dictionary. */ if (Tcl_DictObjGet(interp, dict, dict_map[i].key, &dict_val) == TCL_ERROR) { return TCL_ERROR; } else if (dict_val == NULL) { Tcl_ResetResult(interp); Tcl_AppendResult(interp, "key \"", Tcl_GetString(dict_map[i].key), "\" not known in dictionary", (char*)NULL); return TCL_ERROR; } /* Convert to the proper type and store in a local variable. */ switch (dict_map[i].type) { case NTK_TCL_OBJ: *(Tcl_Obj**)dict_map[i].val = dict_val; break; case NTK_DOUBLE: if (Tcl_GetDoubleFromObj(interp, dict_val, dict_map[i].val) == TCL_ERROR) { return TCL_ERROR; } break; case NTK_INT: if (Tcl_GetIntFromObj(interp, dict_val, dict_map[i].val) == TCL_ERROR) { return TCL_ERROR; } } } /* Silently trim sample_count to range. (Is this a good idea?) */ if (sample_count < 0) { sample_count = 0; } else if (sample_count + ipos > dur * SAMPRATE) { sample_count = dur * SAMPRATE - ipos; } /* Adjust for already-produced samples. */ time += ipos / SAMPRATE; dpos = 1.0 / (dur * SAMPRATE); pos = ipos * dpos; /* Get the output buffer. */ result = Tcl_NewObj(); Tcl_IncrRefCount(result); bytes = Tcl_SetByteArrayLength(result, sample_count * SAMPBYTE); samples = (sample_t*)bytes; /* Link the extra math functions to local variables. */ Tcl_CreateMathFunc(interp, "dur" , 0, NULL, ntk_func, &dur ); Tcl_CreateMathFunc(interp, "pos" , 0, NULL, ntk_func, &pos ); Tcl_CreateMathFunc(interp, "time" , 0, NULL, ntk_func, &time ); Tcl_CreateMathFunc(interp, "phase", 0, NULL, ntk_func, &phase); data->context_dict = dict; /* Calculate each sample. This is where the actual work happens. */ for (sample_index = 0; sample_index < sample_count; ++sample_index) { /* Calculate instantaneous values. */ if (Tcl_ExprDoubleObj(interp, wave_expr, &wave_inst) == TCL_ERROR || Tcl_ExprDoubleObj(interp, ampl_expr, &l_inst) == TCL_ERROR || Tcl_ExprDoubleObj(interp, freq_expr, &freq_inst) == TCL_ERROR) { /* Error in expression. */ code = TCL_ERROR; goto done; } /* Calculate this sample. */ samples[sample_index] = wave_inst * ampl_inst * ampl_base * SAMPAMPL; /* Calculate and apply this frequency. */ phase += freq_inst * freq_base * 2 * M_PI / SAMPRATE; while (phase > 2 * M_PI) { phase -= 2 * M_PI; } /* Next! */ pos += dpos; time += 1.0 / SAMPRATE; } /* Store some stuff back to the dict. */ if (Tcl_DictObjPut(interp, dict, data->phase_key, Tcl_NewDoubleObj(phase)) == TCL_ERROR || Tcl_DictObjPut(interp, dict, data->position_key, Tcl_NewIntObj(ipos + sample_count)) == TCL_ERROR || Tcl_SetVar2Ex(interp, dict_name, NULL, dict, TCL_LEAVE_ERR_MSG) == NULL) { code = TCL_ERROR; goto done; } /* Yahoo. */ code = TCL_OK; Tcl_SetObjResult(interp, result); Tcl_DecrRefCount(result); goto done; done: /* Link the extra math functions to zero. */ Tcl_CreateMathFunc(interp, "dur" , 0, NULL, ntk_func, NULL); Tcl_CreateMathFunc(interp, "pos" , 0, NULL, ntk_func, NULL); Tcl_CreateMathFunc(interp, "time" , 0, NULL, ntk_func, NULL); Tcl_CreateMathFunc(interp, "phase", 0, NULL, ntk_func, NULL); data->context_dict = NULL; return code; } /* Usage: * * ctx variable ?value? * * Gets or sets a variable in the note context dict. */ static int ntk_ctx_cmd(ClientData client_data, Tcl_Interp* interp, int objc, Tcl_Obj* const objv[]) { ntk_client_data_t* data = client_data; Tcl_Obj* dict = data->context_dict; Tcl_Obj* result; if (dict == NULL) { Tcl_SetResult(interp, "::ntk::ctx must be called from inside a" " expression evaluated by ::ntk::gen", TCL_STATIC); return TCL_ERROR; } switch (objc) { case 2: if (Tcl_DictObjGet(interp, dict, objv[1], &result) == TCL_ERROR) { return TCL_ERROR; } if (result == NULL) { Tcl_ResetResult(interp); Tcl_AppendResult(interp, "key \"", Tcl_GetString(objv[1]), "\" not known in dictionary", (char*)NULL); return TCL_ERROR; } break; case 3: result = objv[2]; if (Tcl_DictObjPut(interp, dict, objv[1], result) == TCL_ERROR) { return TCL_ERROR; } break; default: Tcl_WrongNumArgs(interp, 1, objv, "variable ?value?"); return TCL_ERROR; } Tcl_SetObjResult(interp, result); return TCL_OK; } /* Usage: * * mix offset sample_data ?offset sample_data ...? * * Return an unweighted mix of one or more audio buffers. */ static int ntk_mix_cmd(ClientData dummy, Tcl_Interp* interp, int objc, Tcl_Obj* const objv[]) { int i; int length; int offset; int sample_count; int sample_index; Tcl_Obj * result ; /* Return value, as a Tcl object. */ sample_t* dest ; /* Return value, as a sample array. */ sample_t* source ; /* We need an even, nonzero number of arguments. */ if (objc == 1 || objc % 2 != 1) { Tcl_WrongNumArgs(interp, 1, objv, "offset1 samples1 ?offset2 samples2 ...?"); return TCL_ERROR; } /* Determine the length of the output buffer. */ sample_count = 0; for (i = 1; i < objc; i += 2) { if (Tcl_GetIntFromObj(interp, objv[i], &offset) == TCL_ERROR) { return TCL_ERROR; } if (offset < 0) { Tcl_ResetResult(interp); Tcl_AppendResult(interp, "expected nonnegative integer but got \"", Tcl_GetString(objv[i]), "\"", (char*)NULL); return TCL_ERROR; } Tcl_GetByteArrayFromObj(objv[i + 1], &length); length /= SAMPBYTE; if (offset + length > sample_count) { sample_count = offset + length; } } /* Get the output buffer. */ result = Tcl_GetObjResult(interp); dest = (sample_t*)Tcl_SetByteArrayLength(result, sample_count * SAMPBYTE); memset(dest, 0, sample_count * SAMPBYTE); /* Mix it up. */ for (i = 1; i < objc; i += 2) { source = (sample_t*)Tcl_GetByteArrayFromObj(objv[i + 1], &length); length /= SAMPBYTE; Tcl_GetIntFromObj(interp, objv[i], &offset); for (sample_index = 0; sample_index < length; ++sample_index) { dest[sample_index + offset] += source[sample_index]; } } return TCL_OK; } /* Handler for deletion of the gen command. */ static void ntk_gen_cleanup(ClientData client_data) { ntk_client_data_t* data = client_data; Tcl_DecrRefCount(data->start_key ); Tcl_DecrRefCount(data->phase_key ); Tcl_DecrRefCount(data->duration_key ); Tcl_DecrRefCount(data->position_key ); Tcl_DecrRefCount(data->waveform_key ); Tcl_DecrRefCount(data->amplitude_key); Tcl_DecrRefCount(data->ampl_envl_key); Tcl_DecrRefCount(data->frequency_key); Tcl_DecrRefCount(data->freq_envl_key); Tcl_Free(client_data); } /* This function is called upon loading the ntk module. */ int Ntk_Init(Tcl_Interp* interp) { ntk_client_data_t data, *data_p; /* Disable ntk::ctx for the time being. */ data.context_dict = NULL; /* Pregenerate some Tcl_Obj's to be used as dict keys. */ #define MAKE_KEY_OBJ(str) \ do { \ data.str##_key = Tcl_NewStringObj(#str, -1); \ Tcl_IncrRefCount(data.str##_key); \ } while (0) MAKE_KEY_OBJ(start ); MAKE_KEY_OBJ(phase ); MAKE_KEY_OBJ(duration ); MAKE_KEY_OBJ(position ); MAKE_KEY_OBJ(waveform ); MAKE_KEY_OBJ(amplitude); MAKE_KEY_OBJ(ampl_envl); MAKE_KEY_OBJ(frequency); MAKE_KEY_OBJ(freq_envl); #undef MAKE_KEY_OBJ /* Create additional math functions. Make them return zero since they only * make sense when called within gen. */ Tcl_CreateMathFunc(interp, "dur" , 0, NULL, ntk_func, NULL); Tcl_CreateMathFunc(interp, "pos" , 0, NULL, ntk_func, NULL); Tcl_CreateMathFunc(interp, "time" , 0, NULL, ntk_func, NULL); Tcl_CreateMathFunc(interp, "phase", 0, NULL, ntk_func, NULL); /* Make a copy of the client data on the heap. */ data_p = (ntk_client_data_t*)Tcl_Alloc(sizeof(data)); memcpy(data_p, &data, sizeof(data)); /* Now make the actual command. * XXX: What happens if you delete ntk::gen but not ntk::ctx? */ Tcl_CreateObjCommand(interp, "::ntk::gen", ntk_gen_cmd, data_p, ntk_gen_cleanup); Tcl_CreateObjCommand(interp, "::ntk::ctx", ntk_ctx_cmd, data_p, NULL); Tcl_CreateObjCommand(interp, "::ntk::mix", ntk_mix_cmd, NULL, NULL); /* Done. */ return TCL_OK; } /* vim: set ts=4 sts=4 sw=4 tw=80 et: */