Python method for rapidly producing custom synthesized tonal sequences from a terminal window

93 views Asked by At

I'm trying to design a method for generating audio signals rapidly. I need this for electrophysiological experiments in which I will play tone sequences for the purpose of examining neuronal responses in the brain's auditory system.

I need to be able to quickly construct a novel sequence in which I can specify features of each tone (e.g. frequency, duration, amplitude, etc.), silent pauses (i.e. rests), and the sequence of tones and pauses.

I want to do this from the terminal using a simple sequence of codes. For instance, entering tone(440,2) rest(2) tone(880,1) rest(1) tone(880,1) would generate a "song" that plays a 2-second sine wave tone at 440 Hz, then a 2-second rest, then a 1-second tone at 880 Hz, etc.

I have Python functions for producing tones and rests, but I don't know how to access and control them from the terminal for this purpose. After some reading, it seems like using textX or PyParsing might be good options, but I have no background in creating domain-specific languages or parsers, so I'm not sure. I've completed this textX tutorial and read this PyParsing description, but it's not yet clear how or whether I can use these methods for the rapid, terminal-based audio construction and playback that I need. Do you have any suggestions?

2

There are 2 answers

0
Igor Dejanović On

This would be an initial solution for textX:

from textx import metamodel_from_str

grammar = r'''
Commands: commands*=Command;
Command: Tone | Rest;
Tone: 'tone' '(' freq=INT ',' duration=INT ')';
Rest: 'rest' '(' duration=INT ')';
'''

mm = metamodel_from_str(grammar)

input = 'tone(440,2) rest(2) tone(880,1) rest(1) tone(880,1)'
model = mm.model_from_str(input)

for command in model.commands:
    if command.__class__.__name__ == 'Tone':
        # This command is a tone. Call your function for tone. For example:
        render_tone(command.freq, command.duration)
    else:
        # Call rest. For example:
        render_rest(command.duration)

You can also easily take your input recipe from external file by changing above mm.model_from_str to mm.model_from_file.

0
PaulMcG On

This annotated pyparsing example should get you started:

import pyparsing as pp
ppc = pp.pyparsing_common

# expressions for punctuation - useful during parsing, but 
# should be suppressed from the parsed results
LPAR, RPAR, COMMA = map(pp.Suppress, "(),")

# expressions for your commands and numeric values
# the value expression could have used ppc.integer, but
# using number allows for floating point values (such as
# durations that are less than a second)
TONE = pp.Keyword("tone")
REST = pp.Keyword("rest")
value = ppc.number

# expressions for tone and rest commands
tone_expr = (TONE("cmd")
             + LPAR + value("freq") + COMMA + value("duration") + RPAR)
rest_expr = (REST("cmd")
             + LPAR + value("duration") + RPAR)

# a command is a tone or a rest expression
cmd_expr = tone_expr | rest_expr

# functions to call for each command - replace with your actual
# music functions
def play_tone(freq, dur):
    print("BEEP({}, {})".format(freq, dur))

def play_rest(dur):
    print("REST({})".format(dur))

How it works:

cmd_str = "tone(440,0.2) rest(2) tone(880, 1) rest(1) tone( 880, 1 )"

for music_code in cmd_expr.searchString(cmd_str):
    if music_code.cmd == "tone":
        play_tone(music_code.freq, music_code.duration)
    elif music_code.cmd == "rest":
        play_rest(music_code.duration)
    else:
        print("unexpected code", music_code.cmd)

Prints:

BEEP(440, 0.2)
REST(2)
BEEP(880, 1)
REST(1)
BEEP(880, 1)

More info at https://pyparsing-docs.readthedocs.io/en/pyparsing_2.4.7/HowToUsePyparsing.html and module reference at https://pyparsing-docs.readthedocs.io/en/pyparsing_2.4.7/pyparsing.html