diff options
author | Grissess <grissess@nexusg.org> | 2016-06-14 21:22:37 -0400 |
---|---|---|
committer | Grissess <grissess@nexusg.org> | 2016-06-14 21:22:37 -0400 |
commit | 40b1e56164c3b4e95d7d35f5ed800c5626944b01 (patch) | |
tree | f42a6663810f8e8de2dddddd2bdfa70f65b460af | |
parent | e385feff7e1e187a7c74ba2ea6261be46e3e157a (diff) |
Experiment PCM ready
-rw-r--r-- | broadcast.py | 29 | ||||
-rw-r--r-- | client.py | 81 | ||||
-rw-r--r-- | packet.py | 1 |
3 files changed, 88 insertions, 23 deletions
diff --git a/broadcast.py b/broadcast.py index 7d993e6..b0d5efa 100644 --- a/broadcast.py +++ b/broadcast.py @@ -37,12 +37,14 @@ parser.add_option('-B', '--bind-addr', dest='bind_addr', help='The IP address (o parser.add_option('--repeat', dest='repeat', action='store_true', help='Repeat the file playlist indefinitely') parser.add_option('-n', '--number', dest='number', type='int', help='Number of clients to use; if negative (default -1), use the product of stream count and the absolute value of this parameter') parser.add_option('--dry', dest='dry', action='store_true', help='Dry run--don\'t actually search for or play to clients, but pretend they exist (useful with -G)') +parser.add_option('--pcm', dest='pcm', action='store_true', help='Use experimental PCM rendering') +parser.add_option('--pcm-lead', dest='pcmlead', type='float', help='Seconds of leading PCM data to send') parser.add_option('-G', '--gui', dest='gui', default='', help='set a GUI to use') parser.add_option('--pg-fullscreen', dest='fullscreen', action='store_true', help='Use a full-screen video mode') parser.add_option('--pg-width', dest='pg_width', type='int', help='Width of the pygame window') parser.add_option('--pg-height', dest='pg_height', type='int', help='Width of the pygame window') parser.add_option('--help-routes', dest='help_routes', action='store_true', help='Show help about routing directives') -parser.set_defaults(routes=[], test_delay=0.25, random=0.0, rand_low=80, rand_high=2000, live=None, factor=1.0, duration=1.0, volume=1.0, wait_time=0.25, play=[], transpose=0, seek=0.0, bind_addr='', pg_width = 0, pg_height = 0, number=-1) +parser.set_defaults(routes=[], test_delay=0.25, random=0.0, rand_low=80, rand_high=2000, live=None, factor=1.0, duration=1.0, volume=1.0, wait_time=0.25, play=[], transpose=0, seek=0.0, bind_addr='', pg_width = 0, pg_height = 0, number=-1, pcmlead=0.1) options, args = parser.parse_args() if options.help_routes: @@ -314,6 +316,31 @@ if options.repeat: args = itertools.cycle(args) for fname in args: + if options.pcm and not fname.endswith('.iv'): + try: + import audiotools + pcr = audiotools.open(fname).to_pcm() + assert pcr.channels == 1 and pcr.bits_per_sample == 16 and pcr.sample_rate == 44100 + except ImportError: + import wave + pcr = wave.open(fname, 'r') + assert pcr.getnchannels() == 1 and pcr.getsampwidth() == 2 and pcr.getframerate() == 44100 + + BASETIME = time.time() - options.pcmlead + sampcnt = 0 + buf = pcr.read(16).to_bytes(False, True) + while buf: + frag = buf[:32] + buf = buf[32:] + for cl in clients: + s.sendto(struct.pack('>L', CMD.PCM) + frag, cl) + sampcnt += len(frag) / 2 + delay = max(0, BASETIME + (sampcnt / float(pcr.sample_rate)) - time.time()) + #print sampcnt, delay + if delay > 0: + time.sleep(delay) + if not buf: + buf = pcr.read(16).to_bytes(False, True) try: iv = ET.parse(fname).getroot() except IOError: @@ -25,6 +25,7 @@ parser.add_option('-p', '--port', dest='port', type='int', default=13676, help=' parser.add_option('-r', '--rate', dest='rate', type='int', default=44100, help='Set the sample rate of the audio device') parser.add_option('-V', '--volume', dest='volume', type='float', default=1.0, help='Set the volume factor (>1 distorts, <1 attenuates)') parser.add_option('-n', '--streams', dest='streams', type='int', default=1, help='Set the number of streams this client will play back') +parser.add_option('-N', '--numpy', dest='numpy', action='store_true', help='Use numpy acceleration') parser.add_option('-G', '--gui', dest='gui', default='', help='set a GUI to use') parser.add_option('--pg-fullscreen', dest='fullscreen', action='store_true', help='Use a full-screen video mode') parser.add_option('--pg-samp-width', dest='samp_width', type='int', help='Set the width of the sample pane (by default display width / 2)') @@ -33,6 +34,9 @@ parser.add_option('--pg-height', dest='height', type='int', help='Set the height options, args = parser.parse_args() +if options.numpy: + import numpy + PORT = options.port STREAMS = options.streams IDENT = 'TONE' @@ -51,6 +55,7 @@ AMPS = [MAX] * STREAMS MIN = -0x80000000 EXPIRATIONS = [0] * STREAMS +QUEUED_PCM = '' def lin_interp(frm, to, p): return p*to + (1-p)*frm @@ -282,30 +287,57 @@ generator = eval(options.generator) # global FREQ # FREQ = 0 -def lin_seq(frm, to, cnt): - step = (to-frm)/float(cnt) - samps = [0]*cnt - for i in xrange(cnt): - p = i / float(cnt-1) - samps[i] = int(lin_interp(frm, to, p)) - return samps - -def samps(freq, amp, phase, cnt): - global RATE - samps = [0]*cnt - for i in xrange(cnt): - samps[i] = int(amp / math.sqrt(STREAMS) * max(-1, min(1, options.volume*generator((phase + 2 * math.pi * freq * i / RATE) % (2*math.pi))))) - return samps, (phase + 2 * math.pi * freq * cnt / RATE) % (2*math.pi) - -def to_data(samps): - return struct.pack('i'*len(samps), *samps) - -def mix(a, b): - return [min(MAX, max(MIN, i + j)) for i, j in zip(a, b)] +if options.numpy: + lin_seq = numpy.linspace + + def samps(freq, amp, phase, cnt): + samps = numpy.ndarray((cnt,), numpy.int32) + pvel = 2 * math.pi * freq / RATE + fac = amp / float(STREAMS) + for i in xrange(cnt): + samps[i] = fac * max(-1, min(1, generator(phase))) + phase = (phase + pvel) % (2 * math.pi) + return samps, phase + + def to_data(samps): + return samps.tobytes() + + def mix(a, b): + return a + b + +else: + def lin_seq(frm, to, cnt): + step = (to-frm)/float(cnt) + samps = [0]*cnt + for i in xrange(cnt): + p = i / float(cnt-1) + samps[i] = int(lin_interp(frm, to, p)) + return samps + + def samps(freq, amp, phase, cnt): + global RATE + samps = [0]*cnt + for i in xrange(cnt): + samps[i] = int(amp / math.sqrt(STREAMS) * max(-1, min(1, options.volume*generator((phase + 2 * math.pi * freq * i / RATE) % (2*math.pi))))) + return samps, (phase + 2 * math.pi * freq * cnt / RATE) % (2*math.pi) + + def to_data(samps): + return struct.pack('i'*len(samps), *samps) + + def mix(a, b): + return [min(MAX, max(MIN, i + j)) for i, j in zip(a, b)] def gen_data(data, frames, tm, status): - global FREQS, PHASE, Z_SAMP, LAST_SAMP, LAST_SAMPLES - fdata = [0] * frames + global FREQS, PHASE, Z_SAMP, LAST_SAMP, LAST_SAMPLES, QUEUED_PCM + if len(QUEUED_PCM) >= frames*4: + fdata = QUEUED_PCM[:frames*4] + QUEUED_PCM = QUEUED_PCM[frames*4:] + LAST_SAMPLES.extend(struct.unpack(str(frames)+'i', fdata)) + return fdata, pyaudio.paContinue + if options.numpy: + fdata = numpy.zeros((frames,), numpy.int32) + else: + fdata = [0] * frames for i in range(STREAMS): FREQ = FREQS[i] LAST_SAMP = LAST_SAMPS[i] @@ -384,5 +416,10 @@ while True: for i in xrange(len(UID)/4): data[i+2] = stoi(UID[4*i:4*(i+1)]) sock.sendto(str(Packet(CMD.CAPS, *data)), cli) + elif pkt.cmd == CMD.PCM: + fdata = data[4:] + fdata = struct.pack('16i', *[i<<16 for i in struct.unpack('16h', fdata)]) + QUEUED_PCM += fdata + print 'Now', len(QUEUED_PCM) / 4.0, 'frames queued' else: print 'Unknown cmd', pkt.cmd @@ -24,6 +24,7 @@ class CMD: QUIT = 2 # No important data PLAY = 3 # seconds, microseconds, frequency (Hz), amplitude (0.0 - 1.0), port CAPS = 4 # ports, client type (1), user ident (2-7) + PCM = 5 # 16 samples, encoded S16_LE def itos(i): return struct.pack('>L', i) |