Seite 1 von 1

erzeugte Wave Datei ist abgeschnitten...

Verfasst: Freitag 6. September 2013, 16:29
von jens

Code: Alles auswählen

import wave
import array
import logging
import math

log = logging.getLogger("PyDC")
log.setLevel(logging.DEBUG)
log.addHandler(logging.StreamHandler())


WAV_ARRAY_TYPECODE = {
    1: "b", #  8-bit wave file
    2: "h", # 16-bit wave file
    4: "l", # 32-bit wave file TODO: Test it
}

# Maximum volume value in wave files:
MAX_VALUES = {
    1: 255, # 8-bit wave file
    2: 32768, # 16-bit wave file
    4: 2147483647, # 32-bit wave file
}
HUMAN_SAMPLEWIDTH = {
    1: "8-bit",
    2: "16-bit",
    4: "32-bit",
}

def human_duration(t):
    return u"%.1f sec" % round(t, 1)

def sinus_values(count, max_value):
    """
    >>> values = list(sinus_values(10, 32768))
    >>> len(values)
    10
    >>> values
    [0, 21063, 32270, 28378, 11207, -11207, -28378, -32270, -21063, 0]

    >>> tl = TextLevelMeter(32768, width=40)
    >>> for v in values:
    ...     tl.feed(v)
    '|                  *                  |'
    '|                  |           *      |'
    '|                  |                 *|'
    '|                  |               *  |'
    '|                  |     *            |'
    '|            *     |                  |'
    '|  *               |                  |'
    '|*                 |                  |'
    '|      *           |                  |'
    '|                  *                  |'
    """
    count -= 1
    for index in xrange(0, count + 1):
        angle = 360.0 / count * index
        y = math.sin(math.radians(angle)) * max_value
        y = int(round(y))
        yield y

def sinus_values_by_hz(framerate, hz, max_value):
    """
    Create sinus values with the given framerate and Hz.
    Note:
    We skip the first zero-crossing, so the values can be used directy in a loop.

    >>> values = sinus_values_by_hz(22050, 1200, 255)
    >>> len(values) # 22050 / 1200Hz = 18,375
    18
    >>> values
    (87, 164, 221, 251, 251, 221, 164, 87, 0, -87, -164, -221, -251, -251, -221, -164, -87, 0)

    >>> tl = TextLevelMeter(255, width=40)
    >>> for v in values:
    ...     tl.feed(v)
    '|                  |     *            |'
    '|                  |           *      |'
    '|                  |               *  |'
    '|                  |                 *|'
    '|                  |                 *|'
    '|                  |               *  |'
    '|                  |           *      |'
    '|                  |     *            |'
    '|                  *                  |'
    '|            *     |                  |'
    '|      *           |                  |'
    '|  *               |                  |'
    '|*                 |                  |'
    '|*                 |                  |'
    '|  *               |                  |'
    '|      *           |                  |'
    '|            *     |                  |'
    '|                  *                  |'

    >>> values = sinus_values_by_hz(44100, 1200, 255)
    >>> len(values) # 44100 / 1200Hz = 36,75
    37
    """
    count = int(round(float(framerate) / float(hz)))
    count += 1
    values = tuple(sinus_values(count, max_value))
    values = values[1:]
    return values

class BaseConfig(object):
    FRAMERATE = 22050
    SAMPLEWIDTH = 2 # 1 for 8-bit, 2 for 16-bit, 4 for 32-bit samples
    VOLUME_RATIO = 90 # "Loundness" in percent of the created wave file

class WaveBase(object):
    def __init__(self, destination_filepath, cfg):
        self.destination_filepath = destination_filepath
        self.cfg = cfg

        self.wavefile = wave.open(destination_filepath, "wb")

        self.typecode = self.get_typecode(cfg.SAMPLEWIDTH)

        self.wavefile.setnchannels(1) # Mono
        self.wavefile.setsampwidth(cfg.SAMPLEWIDTH)
        self.wavefile.setframerate(cfg.FRAMERATE)

        self.set_wave_properties()

        wave_max_value = MAX_VALUES[self.cfg.SAMPLEWIDTH]
        self.used_max_values = int(round(
            float(wave_max_value) / 100 * self.cfg.VOLUME_RATIO
        ))
        log.info("Create %s wave file with %sHz and %s max volumen (%s%%)" % (
            HUMAN_SAMPLEWIDTH[self.cfg.SAMPLEWIDTH],
            self.cfg.FRAMERATE,
            self.used_max_values, self.cfg.VOLUME_RATIO
        ))

    def get_typecode(self, samplewidth):
        try:
            typecode = WAV_ARRAY_TYPECODE[samplewidth]
        except KeyError:
            raise NotImplementedError(
                "Only %s wave files are supported, yet!" % (
                    ", ".join(["%sBit" % (i * 8) for i in WAV_ARRAY_TYPECODE.keys()])
                )
            )
        return typecode

    def pformat_pos(self):
        sec = float(self.wave_pos) / self.framerate / self.samplewidth
        return "%s (frame no.: %s)" % (human_duration(sec), self.wave_pos)

    def _hz2duration(self, hz):
        return hz2duration(hz, framerate=self.framerate)

    def _duration2hz(self, duration):
        return duration2hz(duration, framerate=self.framerate)

    @property
    def wave_pos(self):
        pos = self.wavefile._nframeswritten * self.samplewidth
        return pos

    def set_wave_properties(self):
        self.framerate = self.wavefile.getframerate() # frames / second
        self.samplewidth = self.wavefile.getsampwidth() # 1 for 8-bit, 2 for 16-bit, 4 for 32-bit samples
        self.max_value = MAX_VALUES[self.samplewidth]
        self.nchannels = self.wavefile.getnchannels() # typically 1 for mono, 2 for stereo

        print "Framerate: %sHz samplewidth: %i (%sBit, max volume value: %s) channels: %s" % (
            self.framerate,
            self.samplewidth, self.samplewidth * 8, self.max_value,
            self.nchannels,
        )
        assert self.nchannels == 1, "Only MONO files are supported, yet!"

    def get_samples(self, hz):
        values = tuple(
            sinus_values_by_hz(self.cfg.FRAMERATE, hz, self.used_max_values)
        )
        real_hz = float(self.cfg.FRAMERATE) / len(values)
        #~ log.debug("Real sinus frequency: %.2fHz" % real_hz)
        return array.array(self.typecode, values)

    def write_sinus(self, hz):
        start_pos = self.pformat_pos()
        samples = self.get_samples(hz)
        print "sinus sample length:", len(samples)
        self.wavefile.writeframes(samples)
        log.debug("Write %sHz sinus %s - %s" % (
            hz, start_pos, self.pformat_pos()
        ))

    def write_silence(self, sec):
        start_pos = self.pformat_pos()
        count = int(round(sec * self.framerate * self.samplewidth))
        silence = [0x00 for _ in xrange(count)]
        silence = array.array(self.typecode, silence)
        self.wavefile.writeframes(silence)
        log.debug("Write %ssec. silence %s - %s" % (
            sec, start_pos, self.pformat_pos()
        ))

    def close(self):
        self.wavefile.close()
        log.info("Wave file '%s' written (%s)" % (
            self.destination_filepath, self.pformat_pos()
        ))

cfg = BaseConfig()
w=WaveBase("test.wav", cfg)
w.write_silence(sec=1)
w.write_sinus(hz=100)
w.write_silence(sec=0.1)
w.write_sinus(hz=200)
w.write_silence(sec=0.1)
w.write_sinus(hz=10)
w.write_silence(sec=1)
w.close()
Ausgabe ist:
Framerate: 22050Hz samplewidth: 2 (16Bit, max volume value: 32768) channels: 1
Create 16-bit wave file with 22050Hz and 29491 max volumen (90%)
Write 1sec. silence 0.0 sec (frame no.: 0) - 1.0 sec (frame no.: 44100)
sinus sample length: 221
Write 100Hz sinus 1.0 sec (frame no.: 44100) - 1.0 sec (frame no.: 44320)
Write 0.1sec. silence 1.0 sec (frame no.: 44320) - 1.1 sec (frame no.: 48730)
sinus sample length: 110
Write 200Hz sinus 1.1 sec (frame no.: 48730) - 1.1 sec (frame no.: 48840)
Write 0.1sec. silence 1.1 sec (frame no.: 48840) - 1.2 sec (frame no.: 53250)
sinus sample length: 2205
Write 10Hz sinus 1.2 sec (frame no.: 53250) - 1.3 sec (frame no.: 55454)
Write 1sec. silence 1.3 sec (frame no.: 55454) - 2.3 sec (frame no.: 99554)
Wave file 'test.wav' written (2.3 sec (frame no.: 99554))
Beim öffnen der wave Datei sollte man eigentlich drei Sinuskurven in Unterschiedlicher Länge sehen.
Ich sehe allerdings nur zwei. Hinten fehlt immer etwas...

Jemand eine Idee warum?

EDIT: Die geöffnete WAVE Datei ist allerdings auch 2,3Sek lang. Sie hat allerdings 49778 Samples. Wobei 99554/2 = 49777 ist. Also eigentlich richtig...

Re: erzeugte Wave Datei ist abgeschnitten...

Verfasst: Freitag 6. September 2013, 16:57
von jens
Ah! hab array.array durch struct.pack ersetzt und nun ist alles richtig.

Code: Alles auswählen

import wave
import logging
import math
import struct

log = logging.getLogger("PyDC")
log.setLevel(logging.DEBUG)
log.addHandler(logging.StreamHandler())


WAV_ARRAY_TYPECODE = {
    1: "b", #  8-bit wave file
    2: "h", # 16-bit wave file
    4: "l", # 32-bit wave file TODO: Test it
}

# Maximum volume value in wave files:
MAX_VALUES = {
    1: 255, # 8-bit wave file
    2: 32768, # 16-bit wave file
    4: 2147483647, # 32-bit wave file
}
HUMAN_SAMPLEWIDTH = {
    1: "8-bit",
    2: "16-bit",
    4: "32-bit",
}

def human_duration(t):
    return u"%.1f sec" % round(t, 1)

def sinus_values(count, max_value):
    """
    >>> values = list(sinus_values(10, 32768))
    >>> len(values)
    10
    >>> values
    [0, 21063, 32270, 28378, 11207, -11207, -28378, -32270, -21063, 0]

    >>> tl = TextLevelMeter(32768, width=40)
    >>> for v in values:
    ...     tl.feed(v)
    '|                  *                  |'
    '|                  |           *      |'
    '|                  |                 *|'
    '|                  |               *  |'
    '|                  |     *            |'
    '|            *     |                  |'
    '|  *               |                  |'
    '|*                 |                  |'
    '|      *           |                  |'
    '|                  *                  |'
    """
    count -= 1
    for index in xrange(0, count + 1):
        angle = 360.0 / count * index
        y = math.sin(math.radians(angle)) * max_value
        y = int(round(y))
        yield y

def sinus_values_by_hz(framerate, hz, max_value):
    """
    Create sinus values with the given framerate and Hz.
    Note:
    We skip the first zero-crossing, so the values can be used directy in a loop.

    >>> values = sinus_values_by_hz(22050, 1200, 255)
    >>> len(values) # 22050 / 1200Hz = 18,375
    18
    >>> values
    (87, 164, 221, 251, 251, 221, 164, 87, 0, -87, -164, -221, -251, -251, -221, -164, -87, 0)

    >>> tl = TextLevelMeter(255, width=40)
    >>> for v in values:
    ...     tl.feed(v)
    '|                  |     *            |'
    '|                  |           *      |'
    '|                  |               *  |'
    '|                  |                 *|'
    '|                  |                 *|'
    '|                  |               *  |'
    '|                  |           *      |'
    '|                  |     *            |'
    '|                  *                  |'
    '|            *     |                  |'
    '|      *           |                  |'
    '|  *               |                  |'
    '|*                 |                  |'
    '|*                 |                  |'
    '|  *               |                  |'
    '|      *           |                  |'
    '|            *     |                  |'
    '|                  *                  |'

    >>> values = sinus_values_by_hz(44100, 1200, 255)
    >>> len(values) # 44100 / 1200Hz = 36,75
    37
    """
    count = int(round(float(framerate) / float(hz)))
    count += 1
    values = tuple(sinus_values(count, max_value))
    values = values[1:]
    return values

class BaseConfig(object):
    FRAMERATE = 22050
    SAMPLEWIDTH = 2 # 1 for 8-bit, 2 for 16-bit, 4 for 32-bit samples
    VOLUME_RATIO = 90 # "Loundness" in percent of the created wave file

class WaveBase(object):
    def __init__(self, destination_filepath, cfg):
        self.destination_filepath = destination_filepath
        self.cfg = cfg

        self.wavefile = wave.open(destination_filepath, "wb")

        self.typecode = self.get_typecode(cfg.SAMPLEWIDTH)

        self.wavefile.setnchannels(1) # Mono
        self.wavefile.setsampwidth(cfg.SAMPLEWIDTH)
        self.wavefile.setframerate(cfg.FRAMERATE)

        self.set_wave_properties()

        wave_max_value = MAX_VALUES[self.cfg.SAMPLEWIDTH]
        self.used_max_values = int(round(
            float(wave_max_value) / 100 * self.cfg.VOLUME_RATIO
        ))
        log.info("Create %s wave file with %sHz and %s max volumen (%s%%)" % (
            HUMAN_SAMPLEWIDTH[self.cfg.SAMPLEWIDTH],
            self.cfg.FRAMERATE,
            self.used_max_values, self.cfg.VOLUME_RATIO
        ))

    def get_typecode(self, samplewidth):
        try:
            typecode = WAV_ARRAY_TYPECODE[samplewidth]
        except KeyError:
            raise NotImplementedError(
                "Only %s wave files are supported, yet!" % (
                    ", ".join(["%sBit" % (i * 8) for i in WAV_ARRAY_TYPECODE.keys()])
                )
            )
        return typecode

    def pformat_pos(self):
        sec = float(self.wave_pos) / self.framerate / self.samplewidth
        return "%s (frame no.: %s)" % (human_duration(sec), self.wave_pos)

    def _hz2duration(self, hz):
        return hz2duration(hz, framerate=self.framerate)

    def _duration2hz(self, duration):
        return duration2hz(duration, framerate=self.framerate)

    @property
    def wave_pos(self):
        pos = self.wavefile._nframeswritten * self.samplewidth
        return pos

    def set_wave_properties(self):
        self.framerate = self.wavefile.getframerate() # frames / second
        self.samplewidth = self.wavefile.getsampwidth() # 1 for 8-bit, 2 for 16-bit, 4 for 32-bit samples
        self.max_value = MAX_VALUES[self.samplewidth]
        self.nchannels = self.wavefile.getnchannels() # typically 1 for mono, 2 for stereo

        print "Framerate: %sHz samplewidth: %i (%sBit, max volume value: %s) channels: %s" % (
            self.framerate,
            self.samplewidth, self.samplewidth * 8, self.max_value,
            self.nchannels,
        )
        assert self.nchannels == 1, "Only MONO files are supported, yet!"

    def write_sinus(self, hz):
        start_pos = self.pformat_pos()

        values = sinus_values_by_hz(
            self.cfg.FRAMERATE, hz, self.used_max_values
        )
        self.write_values(values)

        log.debug("Write %sHz sinus %s - %s" % (
            hz, start_pos, self.pformat_pos()
        ))

    def write_silence(self, sec):
        start_pos = self.pformat_pos()
        count = int(round(sec * self.framerate * self.samplewidth))
        silence = [0x00 for _ in xrange(count)]
        self.write_values(silence)
        log.debug("Write %ssec. silence %s - %s" % (
            sec, start_pos, self.pformat_pos()
        ))

    def write_values(self, values):
        samples = "".join([struct.pack(self.typecode, value) for value in values])
        self.wavefile.writeframes(samples)

    def close(self):
        self.wavefile.close()
        log.info("Wave file '%s' written (%s)" % (
            self.destination_filepath, self.pformat_pos()
        ))

cfg = BaseConfig()
w=WaveBase("test.wav", cfg)
w.write_silence(sec=1)
w.write_sinus(hz=100)
w.write_silence(sec=0.1)
w.write_sinus(hz=200)
w.write_silence(sec=0.1)
w.write_sinus(hz=10)
w.write_silence(sec=1)
w.close()

Re: erzeugte Wave Datei ist abgeschnitten...

Verfasst: Freitag 6. September 2013, 17:04
von jens
Ach und schneller und schöner geht es damit:

Code: Alles auswählen

    def write_values(self, values):
        values = tuple(values) # for iterator/generator objects
        value_length = len(values)
        pack_format = "%i%s" % (value_length, self.typecode)
        samples = struct.pack(pack_format, *values)
        self.wavefile.writeframes(samples)
Frage: Wie kann man das einfacher machen:

Code: Alles auswählen

silence = [0x00 for _ in xrange(count)]
???

Re: erzeugte Wave Datei ist abgeschnitten...

Verfasst: Freitag 6. September 2013, 17:09
von BlackJack
@writeframes: Alternativ hätte man das `array` vielleicht in eine Zeichen-/Bytekette umwandeln können bevor man es schreibt.

``silence = [0] * length``?

Re: erzeugte Wave Datei ist abgeschnitten...

Verfasst: Freitag 6. September 2013, 17:28
von jens
BlackJack hat geschrieben:``silence = [0] * length``?
Gute Idee, danke! ...und übernommen: https://github.com/jedie/PyDragon32/com ... b83#L2L575

Ich dachte das ergebnis von array.array und struct.pack wäre quasi das gleiche...